getErrorType.mjs

import get from 'lodash-es/get.js'
import isestr from 'wsemi/src/isestr.mjs'


// getErrorType.mjs — 由失敗結果推導機器可讀之errorType
//
// 【為何需要】失敗分類原本只能靠error字串前綴(TIMEOUT/ENOENT/OUTPUT_VALIDATION_FAILED...)
//   比對, 呼叫端各自重寫字串規則既脆弱又易漂移。errorType提供穩定的機器可讀分類,
//   error字串保留不動(人讀), 兩者並存。
//
// 【只判機械可判者, 不猜測】本函數僅涵蓋「由error字樣/結構即可100%確定」的類別,
//   與dispatchAiFallback之isKeyIndependentFail同一組機械判準; 各家CLI的其餘失敗
//   (額度上限/金鑰無效/服務端錯誤等, 字樣各家不同且隨版本漂移)一律歸'exec',
//   不維護簽章表(與否決金鑰停用清單同一理由, 呼叫端需細分時用coolDetect式注入自判)。
//
// 【errorType一覽(僅失敗結果帶此欄, 成功結果無)】
//   'params'           參數/設定檢核失敗(進入執行前即被擋, getErrorResult預設)
//   'timeout'          逾時(execCli強殺或API abort, error以TIMEOUT開頭)
//   'spawn'            子進程無法啟動(執行檔不存在ENOENT/命令列過長ENAMETOOLONG)
//   'validation'       stdout未過validate(OUTPUT_VALIDATION_FAILED)
//   'exec'             CLI非零離開碼之一般執行失敗(未能再機械細分)
//   'http'             HTTP非2xx(code為狀態碼, 僅api類)
//   'fetch'            網路層錯誤(DNS/連線拒絕, 僅api類)
//   'tool-unsupported' 模型回tool_calls而本轉接器不支援工具(僅api類)
//   'invalid-response' 回應結構不合規(缺choices[0].message.content、缺output陣列,
//                      或api-typesafe-systemone缺answers物件/缺所請求題目之答案, 僅api類);
//                      HTTP 200但本體非JSON時error另為'INVALID_RESPONSE: body is not JSON (...)'(附位元組資訊,
//                      遞補層整組跳過; 見describeNonJsonBody.mjs), 「JSON缺欄位」則仍換金鑰
//   'incomplete'       回應未完整: 截斷(chat/completions之finish_reason為length/content_filter、
//                      Responses API之status為incomplete; 結果另帶truncated:true, 規則見checkTruncation.mjs)
//                      與Responses API之其餘非completed狀態(如failed; truncated:false)。僅api類;
//                      半截內容預設不當成功回傳, 呼叫端明示acceptTruncated才可放行(2026-09-24起兩轉接器同規則)
//   'aborted'          shouldStop中止(僅dispatchAiFallback)
//   'budget'           時間預算用盡(僅dispatchAiFallback)


/**
 * 由失敗結果物件推導機器可讀之errorType(僅機械可判者,其餘歸'exec',不猜測)
 *
 * 判準與dispatchAiFallback之isKeyIndependentFail同組:error以TIMEOUT開頭為'timeout'、
 * 含ENOENT或ENAMETOOLONG為'spawn'、恰為OUTPUT_VALIDATION_FAILED為'validation',
 * 其餘失敗一律'exec'(各家CLI字樣不同且隨版本漂移,不維護簽章表)
 *
 * @param {Object} r 輸入失敗結果物件(取其error欄位判別)
 * @returns {String} 回傳errorType字串
 * @example
 *
 * import getErrorType from './src/getErrorType.mjs'
 *
 * console.log(getErrorType({ error: 'TIMEOUT after 300s' }))
 * // => 'timeout'
 *
 * console.log(getErrorType({ error: 'spawn cli ENOENT' }))
 * // => 'spawn'
 *
 * console.log(getErrorType({ error: 'Exit code 1' }))
 * // => 'exec'
 *
 */
function getErrorType(r) {
    let error = get(r, 'error', '')
    if (!isestr(error)) {
        return 'exec'
    }
    if (error.indexOf('TIMEOUT') === 0) {
        return 'timeout'
    }
    if (error.includes('ENOENT') || error.includes('ENAMETOOLONG')) {
        return 'spawn'
    }
    if (error === 'OUTPUT_VALIDATION_FAILED') {
        return 'validation'
    }
    return 'exec'
}


/**
 * 失敗結果補上errorType欄位(已帶有效errorType或成功結果則原樣回傳)
 *
 * @param {Object} r 輸入結果物件
 * @returns {Object} 回傳結果物件,失敗且未帶errorType時追加之
 * @example
 *
 * import { attachErrorType } from './src/getErrorType.mjs'
 *
 * console.log(attachErrorType({ ok: false, error: 'TIMEOUT after 10s' }).errorType)
 * // => 'timeout'
 *
 * console.log(attachErrorType({ ok: true, stdout: 'abc' }).errorType)
 * // => undefined
 *
 */
function attachErrorType(r) {
    if (get(r, 'ok', false) === true) {
        return r
    }
    if (isestr(get(r, 'errorType', null))) {
        return r
    }
    return { ...r, errorType: getErrorType(r) }
}


export default getErrorType
export { attachErrorType }