decolua/9router · error

Kiro tool input must be a JSON object

Error message

Kiro tool input must be a JSON object

What it means

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.

Source

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

      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");
      if (tool.inputKind === "object") return tool.inputObject;
      try {
        const input = JSON.parse(tool.inputChunks.join(""));
        if (!input || typeof input !== "object" || Array.isArray(input)) throw new Error("not an object");
        return input;
      } catch (error) {
        throw new Error(`Kiro tool input must be valid object JSON (${error.message})`);
      }
    };
    const emitTools = (controller) => {
      for (const tool of state.tools.values()) {
        // Validate per tool, not per turn: one unusable fragment used to throw out
        // of emitTools and take every other complete tool call in the same turn

View on GitHub (pinned to 90b52e06ff)

Solutions

  1. Inspect the `[Codex Reset Credits] codex: <msg>` log line or response body for the root cause.
  2. Re-authorize the Codex connection if the message indicates authentication/401 — refresh token may be invalid.
  3. Verify the connection proxy URL is reachable, or disable the connection proxy for this connection.
  4. Retry — transient fetch errors resolve on a second attempt (a credit is only consumed on success).
  5. If the upstream API changed, update the consume/redeem logic in open-sse/services/usage.js.
Defensive patterns

Strategy: try-catch

Validate before calling

// preflight before POSTing a credit consume
const conn = await getProviderConnectionById(connectionId);
if (!conn || conn.provider !== 'codex') return;
if (conn.authType === 'oauth' && !conn.refreshToken) console.warn('no refresh token — POST may 500 on refresh');
const creditCheck = await fetch(`/api/usage/${connectionId}/codex-reset-credits`);
if (!creditCheck.ok) console.warn('credits fetch failing; consume POST likely to fail too');

Type guard

function isNoCreditResponse(payload) {
  return payload?.code === 'no_credit'; // 409 — distinct from thrown 500 errors
}

Try / catch

const res = await fetch(`/api/usage/${connectionId}/codex-reset-credits`, { method: 'POST' });
if (res.status === 500) {
  const { error } = await res.json();
  if (/unauthorized|expired|401|re-?authorize/i.test(error)) await reauthorizeCodexConnection();
  // otherwise: check proxy config / upstream reachability, then retry
} else if (res.status === 409) {
  // no_credit — expected, not an error
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


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