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 turnView on GitHub (pinned to 90b52e06ff)
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.
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
- 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.
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
- No Codex access token available. Please re-authorize the con
- Missing accessToken
- Failed to save tokens
- ${callbackParams.error_description || callbackParams.error}
- No authorization code received
AI-assisted analysis of decolua/9router@90b52e06ff (2026-08-30).
Data as JSON: /api/errors/2cc4a19264e0be71.
Report an issue: GitHub.