decolua/9router · error

Kiro tool input changed fragment type

Error message

Kiro tool input changed fragment type

What it means

handleStreamingResponse expected an SSE (text/event-stream) response from the upstream provider but got something else (HTML error page, JSON error, login page, empty body). It logs `BLOCKED <status> · provider/model · non-SSE (<content-type>)`, signals streamController.handleError, and returns an HTTP response with status equal to the upstream status (default 502) and message `[<status>]: <shortMsg>`, where shortMsg is the sanitized <title> or the trimmed body text.

Source

Thrown at open-sse/executors/kiro.js:690

      state.terminalProvenance = provenance;
      state.transportState = extra.transport_state || "corrupt_frame";
      const detail = diagnostics({
        stop_disposition: extra.stop_disposition || "terminal_incomplete",
        ...extra
      });
      options.onTerminalState?.(detail);
      controller.enqueue(encodeSSEError(code, message, detail));
    };
    const assertToolBufferBound = () => {
      if (state.bufferedToolBytes <= (options.maxToolBytes || KIRO_REPAIR_BUFFER_MAX_BYTES / 2)) return;
      const error = new Error("Kiro buffered tool input exceeded the integrity memory bound");
      error.code = "KIRO_BUFFER_EXCEEDED";
      throw error;
    };
    const appendToolInput = (tool, input) => {
      if (input === undefined) return;
      if (typeof input === "string") {
        if (tool.inputKind && tool.inputKind !== "string") throw new Error("Kiro tool input changed fragment type");
        tool.inputKind = "string";
        tool.inputChunks ||= [];
        tool.inputChunks.push(input);
        state.bufferedToolBytes += encoder.encode(input).byteLength;
      } else if (input && typeof input === "object" && !Array.isArray(input)) {
        if (tool.inputKind && tool.inputKind !== "object") throw new Error("Kiro tool input changed fragment type");
        tool.inputKind = "object";
        state.bufferedToolBytes -= tool.inputBytes || 0;
        tool.inputObject = input;
        tool.inputBytes = encoder.encode(JSON.stringify(input)).byteLength;
        state.bufferedToolBytes += tool.inputBytes;
      } else {
        throw new Error("Kiro tool input must be a JSON object");
      }
      assertToolBufferBound();
    };
    const parsedToolInput = (tool) => {
      if (!tool.inputKind) throw new Error("Kiro tool call is missing input");

View on GitHub (pinned to 90b52e06ff)

Solutions

  1. Read shortMsg in the error body — it contains the upstream <title> or body text identifying the real problem.
  2. If it's a login/401 page, refresh the provider connection's credentials (OAuth re-auth or new API key) in the dashboard.
  3. If 429/quota, wait or switch to a fallback model/account (combo fallback).
  4. If Cloudflare/WAF HTML, retry, change IP/proxy, or use a connection proxy configured for that provider.
  5. Verify the provider's base URL points at its API endpoint, not the website.

Example fix

// before: raw upstream HTML/JSON surfaces opaquely
const status = providerResponse.status || 502;
// after: check credentials proactively so upstream never returns an HTML error page
if (!connection.accessToken) throw new Error('provider credentials missing');
const status = providerResponse.status || 502;
Defensive patterns

Strategy: type-guard

Validate before calling

// inspect the structured error the router returns before retrying
const body = await res.json();
const m = /^\[(\d+)\]:\s(.*)$/.exec(body.error?.message || '');
if (m) {
  const [_, upstreamStatus, detail] = m;
  if (upstreamStatus === '401' || /sign in|login/i.test(detail)) await reauthorizeProvider();
  else if (upstreamStatus === '429') await backoff();
}

Type guard

function isNonSseUpstreamError(payload) {
  return typeof payload?.error?.message === 'string'
    && /^\[\d+\]:/.test(payload.error.message);
}

Try / catch

const res = await fetchChat(model, messages);
if (!res.ok || !(res.headers.get('content-type') || '').includes('event-stream')) {
  const body = await res.json().catch(() => ({}));
  if (isNonSseUpstreamError(body)) {
    const [status, detail] = body.error.message.match(/^\[(\d+)\]:\s(.*)$/).slice(1);
    // 401/403 → refresh credentials; 429 → backoff/fallback; 5xx → retry/proxy
  }
}

Prevention

When it happens

Trigger: An SSE chat completion request reaches an upstream that answers with a non-SSE content type — e.g. 401/403 HTML login page, 429 rate-limit JSON, 502/503 Cloudflare error page, or a captive-portal/CDN block — and the provider response passes through the streaming path.

Common situations: Expired/invalid provider credentials (login HTML returned); rate limiting or quota exhaustion; Cloudflare/WAF challenge pages; wrong base URL pointing at a website instead of the API; region blocks; upstream outage returning an error page.

Related errors


AI-assisted analysis of decolua/9router@90b52e06ff (2026-08-30). Data as JSON: /api/errors/6dea011184e3e014. Report an issue: GitHub.