{"record":{"id":"2cc4a19264e0be71","repo":"decolua/9router","slug":"kiro-tool-input-must-be-a-json-object","errorCode":null,"errorMessage":"Kiro tool input must be a JSON object","messagePattern":"Kiro tool input must be a JSON object","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"open-sse/executors/kiro.js","lineNumber":703,"sourceCode":"      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\");\n      if (tool.inputKind === \"object\") return tool.inputObject;\n      try {\n        const input = JSON.parse(tool.inputChunks.join(\"\"));\n        if (!input || typeof input !== \"object\" || Array.isArray(input)) throw new Error(\"not an object\");\n        return input;\n      } catch (error) {\n        throw new Error(`Kiro tool input must be valid object JSON (${error.message})`);\n      }\n    };\n    const emitTools = (controller) => {\n      for (const tool of state.tools.values()) {\n        // Validate per tool, not per turn: one unusable fragment used to throw out\n        // of emitTools and take every other complete tool call in the same turn","sourceCodeStart":685,"sourceCodeEnd":721,"githubUrl":"https://github.com/decolua/9router/blob/90b52e06ffd666b7929554211474d01588f6b1f8/open-sse/executors/kiro.js#L685-L721","documentation":"POST handler of the Codex reset-credits API: after refreshing credentials (if needed), consuming a reset credit via consumeCodexRateLimitResetCredit, and redeeming, any uncaught exception is logged as `[Codex Reset Credits] <provider>: <msg>` and returned as HTTP 500 with error.message. Documented non-OK consume outcomes (e.g. no_credit → 409) are handled separately; this 500 only fires on thrown errors.","triggerScenarios":"POST /api/usage/:connectionId/codex-reset-credits where refreshCodexConnection's refreshAndUpdateCredentials throws outside its own wrapper, consumeCodexRateLimitResetCredit throws (network/proxy failure, non-2xx from Codex), or the redeem request path throws unexpectedly.","commonSituations":"Revoked/expired OAuth where automatic refresh fails; connection proxy misconfigured; Codex redeem endpoint rejecting the request in a way that surfaces as an exception; transient network failure mid-consume.","solutions":["Inspect the `[Codex Reset Credits] codex: <msg>` log line or response body for the root cause.","Re-authorize the Codex connection if the message indicates authentication/401 — refresh token may be invalid.","Verify the connection proxy URL is reachable, or disable the connection proxy for this connection.","Retry — transient fetch errors resolve on a second attempt (a credit is only consumed on success).","If the upstream API changed, update the consume/redeem logic in open-sse/services/usage.js."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"// preflight before POSTing a credit consume\nconst conn = await getProviderConnectionById(connectionId);\nif (!conn || conn.provider !== 'codex') return;\nif (conn.authType === 'oauth' && !conn.refreshToken) console.warn('no refresh token — POST may 500 on refresh');\nconst creditCheck = await fetch(`/api/usage/${connectionId}/codex-reset-credits`);\nif (!creditCheck.ok) console.warn('credits fetch failing; consume POST likely to fail too');","typeGuard":"function isNoCreditResponse(payload) {\n  return payload?.code === 'no_credit'; // 409 — distinct from thrown 500 errors\n}","tryCatchPattern":"const res = await fetch(`/api/usage/${connectionId}/codex-reset-credits`, { method: 'POST' });\nif (res.status === 500) {\n  const { error } = await res.json();\n  if (/unauthorized|expired|401|re-?authorize/i.test(error)) await reauthorizeCodexConnection();\n  // otherwise: check proxy config / upstream reachability, then retry\n} else if (res.status === 409) {\n  // no_credit — expected, not an error\n}","preventionTips":["Distinguish 409 no_credit (expected) from 500 (thrown failure) before reacting.","Keep Codex OAuth refresh tokens valid — failed refresh is the most common 500 source.","Test the connection proxy before relying on it for credit consumption.","Retry transient failures — a credit is only consumed on a successful redeem."],"tags":["api","codex","oauth","credits","upstream-fetch"],"backgroundTag":"oauth-token-expired","analyzedSha":"90b52e06ffd666b7929554211474d01588f6b1f8","analyzedAt":"2026-08-30T21:05:45.952Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}