{"record":{"id":"6dea011184e3e014","repo":"decolua/9router","slug":"kiro-tool-input-changed-fragment-type","errorCode":null,"errorMessage":"Kiro tool input changed fragment type","messagePattern":"Kiro tool input changed fragment type","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"open-sse/executors/kiro.js","lineNumber":690,"sourceCode":"      state.terminalProvenance = provenance;\n      state.transportState = extra.transport_state || \"corrupt_frame\";\n      const detail = diagnostics({\n        stop_disposition: extra.stop_disposition || \"terminal_incomplete\",\n        ...extra\n      });\n      options.onTerminalState?.(detail);\n      controller.enqueue(encodeSSEError(code, message, detail));\n    };\n    const assertToolBufferBound = () => {\n      if (state.bufferedToolBytes <= (options.maxToolBytes || KIRO_REPAIR_BUFFER_MAX_BYTES / 2)) return;\n      const error = new Error(\"Kiro buffered tool input exceeded the integrity memory bound\");\n      error.code = \"KIRO_BUFFER_EXCEEDED\";\n      throw error;\n    };\n    const appendToolInput = (tool, input) => {\n      if (input === undefined) return;\n      if (typeof input === \"string\") {\n        if (tool.inputKind && tool.inputKind !== \"string\") throw new Error(\"Kiro tool input changed fragment type\");\n        tool.inputKind = \"string\";\n        tool.inputChunks ||= [];\n        tool.inputChunks.push(input);\n        state.bufferedToolBytes += encoder.encode(input).byteLength;\n      } else if (input && typeof input === \"object\" && !Array.isArray(input)) {\n        if (tool.inputKind && tool.inputKind !== \"object\") throw new Error(\"Kiro tool input changed fragment type\");\n        tool.inputKind = \"object\";\n        state.bufferedToolBytes -= tool.inputBytes || 0;\n        tool.inputObject = input;\n        tool.inputBytes = encoder.encode(JSON.stringify(input)).byteLength;\n        state.bufferedToolBytes += tool.inputBytes;\n      } else {\n        throw new Error(\"Kiro tool input must be a JSON object\");\n      }\n      assertToolBufferBound();\n    };\n    const parsedToolInput = (tool) => {\n      if (!tool.inputKind) throw new Error(\"Kiro tool call is missing input\");","sourceCodeStart":672,"sourceCodeEnd":708,"githubUrl":"https://github.com/decolua/9router/blob/90b52e06ffd666b7929554211474d01588f6b1f8/open-sse/executors/kiro.js#L672-L708","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Read shortMsg in the error body — it contains the upstream <title> or body text identifying the real problem.","If it's a login/401 page, refresh the provider connection's credentials (OAuth re-auth or new API key) in the dashboard.","If 429/quota, wait or switch to a fallback model/account (combo fallback).","If Cloudflare/WAF HTML, retry, change IP/proxy, or use a connection proxy configured for that provider.","Verify the provider's base URL points at its API endpoint, not the website."],"exampleFix":"// before: raw upstream HTML/JSON surfaces opaquely\nconst status = providerResponse.status || 502;\n// after: check credentials proactively so upstream never returns an HTML error page\nif (!connection.accessToken) throw new Error('provider credentials missing');\nconst status = providerResponse.status || 502;","handlingStrategy":"type-guard","validationCode":"// inspect the structured error the router returns before retrying\nconst body = await res.json();\nconst m = /^\\[(\\d+)\\]:\\s(.*)$/.exec(body.error?.message || '');\nif (m) {\n  const [_, upstreamStatus, detail] = m;\n  if (upstreamStatus === '401' || /sign in|login/i.test(detail)) await reauthorizeProvider();\n  else if (upstreamStatus === '429') await backoff();\n}","typeGuard":"function isNonSseUpstreamError(payload) {\n  return typeof payload?.error?.message === 'string'\n    && /^\\[\\d+\\]:/.test(payload.error.message);\n}","tryCatchPattern":"const res = await fetchChat(model, messages);\nif (!res.ok || !(res.headers.get('content-type') || '').includes('event-stream')) {\n  const body = await res.json().catch(() => ({}));\n  if (isNonSseUpstreamError(body)) {\n    const [status, detail] = body.error.message.match(/^\\[(\\d+)\\]:\\s(.*)$/).slice(1);\n    // 401/403 → refresh credentials; 429 → backoff/fallback; 5xx → retry/proxy\n  }\n}","preventionTips":["Keep provider credentials fresh — most non-SSE responses are login/401 HTML pages.","Configure combo/account fallback so 429/5xx upstreams fail over automatically.","Point provider base URLs at the API endpoint, never the marketing site.","Route providers through a connection proxy when Cloudflare/WAF challenges recur.","Parse the `[status]: detail` message format — the embedded title/body names the true cause."],"tags":["sse","streaming","upstream","non-sse-response","proxy"],"backgroundTag":"upstream-non-sse-response","analyzedSha":"90b52e06ffd666b7929554211474d01588f6b1f8","analyzedAt":"2026-08-30T21:05:45.952Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}