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.

StreamChunk 分片序列图

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

// Ruta del archivo: código de ejemplo que demuestra una secuencia completa de chunks
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.

ReglaDescripción
block-start y block-end van en paresCada block-start debe tener su block-end correspondiente
index comienza en 0 y se incrementaSe usa para identificar el orden de los bloques de contenido
argumentsDelta es el incremento del JSON originalPuede generarse completo en un fragmento o en varios fragmentos
finish debe ser el último fragmentoDespués no puede haber ningún fragmento
usage debe ir antes de finishPrimero 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

// Ruta del archivo: código de ejemplo, un esqueleto de HttpAdapter con manejo de errores
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