DeepSeek Harness: Protocolo StreamChunk y manejo de errores
¿Qué datos emite realmente stream() del adaptador?
La respuesta es StreamChunk, un protocolo de fragmentación con un orden estricto.
Este artículo aborda la secuencia de fragmentos, las reglas clave y cómo manejar los errores de transmisión.
Protocolo StreamChunk
StreamChunk es el protocolo de streaming entre Harness y el adaptador.
Un bloque de contenido comienza con block-start, se transmite de forma incremental mediante delta y termina con block-end.
El texto y las llamadas a herramientas son dos tipos distintos de bloques de contenido; cada uno recorre start / delta / end por separado.
Al finalizar todos los fragmentos, primero se envía usage para reportar el consumo de tokens y luego finish para declarar el motivo de finalización.
La figura anterior, de arriba hacia abajo, muestra una secuencia completa de fragmentos.
Primero un bloque de texto: block-start → text-delta × 2 → block-end.
Luego un bloque de llamada a herramienta: block-start → tool-call-delta → block-end.
Finalmente usage y finish.
El reason de finish con valor stop indica finalización normal; con valor tool-calls indica que se solicita ejecutar una herramienta.
Secuencia completa de fragmentos
La documentación oficial proporciona exampleChunks, que produce todos los chunks de una generación en orden.
text-delta puede dividirse en varios fragmentos que se concatenan de forma incremental hasta formar el texto completo.
argumentsDelta de tool-call-delta es el incremento del texto JSON original.
Ejemplo
import { CallId, type StreamChunk } from '@deepseek-ai/dsh-llm'
async function* exampleChunks(): AsyncIterable<StreamChunk> {
// 1. Start each content block with block-start.
// Inicia un bloque de texto con index 0
yield { type: 'block-start', index: 0, blockType: 'text' }
// 2. Stream text through text-delta.
// Incremento de texto; se puede dividir en varios fragmentos
yield { type: 'text-delta', index: 0, text: 'ejemplo' }
yield { type: 'text-delta', index: 0, text: ' tutorial' }
// 3. End each content block with block-end and the complete block.
// Cierra con un bloque completo; index coincide con block-start
yield {
type: 'block-end',
index: 0,
block: { type: 'text', text: 'ejemplo tutorial' },
}
// 4. Tool-call block.
// Inicia un bloque de llamada a herramienta con index 1
yield { type: 'block-start', index: 1, blockType: 'tool-call' }
// Nombre de la herramienta e incremento de argumentos; el id se genera con la fábrica CallId
yield {
type: 'tool-call-delta',
index: 1,
id: CallId('call-123'),
name: 'bash',
argumentsDelta: '{"command":"echo ejemplo"}',
}
// Cierra con un bloque completo; arguments es el texto JSON ensamblado
yield {
type: 'block-end',
index: 1,
block: {
type: 'tool-call',
id: CallId('call-123'),
name: 'bash',
arguments: '{"command":"echo ejemplo"}',
},
}
// 5. Token usage.
// Reporta el consumo de tokens; debe ir antes de finish
yield { type: 'usage', usage: { inputTokens: 100, outputTokens: 50 } }
// 6. Finish reason.
// Último fragmento; declara el motivo de finalización
yield { type: 'finish', reason: { kind: 'stop' } }
// Alternatively, { kind: 'tool-calls' } requests tool execution.
}
CallId es una función de fábrica incorporada en el protocolo que se usa para generar ids de llamadas a herramientas.
argumentsDelta puede generarse completo en un solo fragmento o de forma incremental en varios fragmentos.
Reglas clave
El protocolo tiene cinco reglas estrictas; violar cualquiera de ellas hará que el consumidor falle al parsear.
| Regla | Descripción |
|---|---|
| block-start y block-end van en pares | Cada block-start debe tener su block-end correspondiente |
| index comienza en 0 y se incrementa | Se usa para identificar el orden de los bloques de contenido |
| argumentsDelta es el incremento del JSON original | Puede generarse completo en un fragmento o en varios fragmentos |
| finish debe ser el último fragmento | Después no puede haber ningún fragmento |
| usage debe ir antes de finish | Primero se reporta el consumo de tokens y luego se declara la finalización |
text-delta y tool-call-delta deben llevar el index de su bloque correspondiente; no entremezcles bloques de contenido.
Manejo de errores: expresar fallos con LlmError
El adaptador debe lanzar los fallos de transmisión y protocolo mediante LlmError con code estable.
agent-loop conserva ese error y su code para diagnóstico y manejo de políticas.
No dependas de que un Error común se convierta automáticamente.
Un code estable permite que la capa superior identifique con precisión el tipo de error, en lugar de analizar cadenas de error.
Cada solicitud HTTP al proveedor también debe fusionar attributionHeaders() en sus encabezados.
También se debe pasar options.signal a fetch para que el proceso de cancelación y liberación de recursos se detenga por completo.
Ejemplo
import {
attributionHeaders,
LlmAdapter,
LlmError,
type GenerateOptions,
type StreamChunk,
} from '@deepseek-ai/dsh-llm'
class HttpAdapter extends LlmAdapter {
// El constructor inyecta la dirección del endpoint
constructor(private readonly endpoint: string) {
super()
}
async *stream(options: GenerateOptions): AsyncIterable<StreamChunk> {
// Realiza la solicitud HTTP, fusiona los encabezados de atribución y pasa la señal de aborto
const response = await fetch(this.endpoint, {
method: 'POST',
headers: {
'content-type': 'application/json',
...attributionHeaders(),
},
body: JSON.stringify({ model: options.model, messages: options.messages }),
// Cuando el llamador solicita la cancelación, fetch se aborta de inmediato
...options.signal ? { signal: options.signal } : {},
})
if (!response.ok) {
// Expresa el fallo de transmisión con un LlmError de code estable
throw new LlmError(`Provider API error: ${response.status}`, 'PROVIDER_HTTP_ERROR')
}
// A real adapter parses the response and emits the complete chunk sequence.
// El adaptador real analiza el cuerpo de la respuesta aquí y produce la secuencia completa de fragmentos
yield { type: 'finish', reason: { kind: 'stop' } }
}
}
attributionHeaders() fusiona la información de atribución en los encabezados de la solicitud.
Cuando options.signal existe, se pasa como signal a fetch; al cancelar la solicitud, se detiene de inmediato.
Cuando response.ok es false, se lanza LlmError con code PROVIDER_HTTP_ERROR.
El esqueleto solo emite (yield) un finish; el adaptador real analiza el cuerpo de la respuesta aquí.
El primer parámetro de LlmError es el mensaje; el segundo es el code estable.
La capa superior toma decisiones de política según el code, por lo que, una vez publicado, el code no debe cambiarse.
Campos que no deben descartarse silenciosamente
Si GenerateOptions contiene campos que el adaptador no puede admitir, también se debe lanzar LlmError; no los descartes silenciosamente.
Los metadatos de razonamiento contienen IDs opacos ordenados, nombres para mostrar y valores predeterminados de configuración opcionales.
Conserva la lista opcional autoritativa que proporciona el adaptador, incluido el off que devuelve su API de capacidades upstream.
No conviertas la intensidad de razonamiento opcional en una enumeración central; de lo contrario, el adaptador perderá la flexibilidad del upstream.
El servicio valida los resultados agregados y rechaza las intensidades de razonamiento especificadas explícitamente pero no admitidas antes de llamar a stream().
Omitir reasoning indica que el modelo no tiene capacidades opcionales de intensidad de razonamiento.
Resumen y autoevaluación
Resumen en una frase: el protocolo StreamChunk describe el contenido con bloques emparejados y cierra con usage y finish; los fallos se expresan con un LlmError de code estable.
Autoevaluación 1: ¿qué fragmentos se necesitan como mínimo para un bloque de texto?
Autoevaluación 2: ¿cuál es el orden entre usage y finish?
Autoevaluación 3: cuando falla una solicitud, ¿por qué usar LlmError con code en lugar de un Error común?
Otras extensiones