w-dispatch-ai 1.0.25 → 1.0.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/README.md +56 -4
  2. package/dist/w-dispatch-ai.umd.js +2 -2
  3. package/dist/w-dispatch-ai.umd.js.map +1 -1
  4. package/docs/WDispatchAi.mjs.html +6 -4
  5. package/docs/adapters.mjs.html +10 -3
  6. package/docs/budgetFor.mjs.html +2 -2
  7. package/docs/buildValidator.mjs.html +2 -2
  8. package/docs/castPintOr.mjs.html +2 -2
  9. package/docs/dfTimeoutMs.mjs.html +2 -2
  10. package/docs/dispatchAi.mjs.html +2 -2
  11. package/docs/dispatchAiFallback.mjs.html +2 -2
  12. package/docs/dispatchAiWkf.mjs.html +2 -2
  13. package/docs/dispatchAntigravity.mjs.html +2 -2
  14. package/docs/dispatchApiOpenaiCompat.mjs.html +2 -2
  15. package/docs/dispatchApiOpenaiResponses.mjs.html +2 -2
  16. package/docs/dispatchApiTypesafeSystemone.mjs.html +447 -0
  17. package/docs/dispatchClaude.mjs.html +2 -2
  18. package/docs/dispatchCodex.mjs.html +2 -2
  19. package/docs/dispatchOpencode.mjs.html +2 -2
  20. package/docs/getCliArgs.mjs.html +2 -2
  21. package/docs/getErrorResult.mjs.html +2 -2
  22. package/docs/getErrorType.mjs.html +4 -3
  23. package/docs/global.html +1221 -254
  24. package/docs/index.html +2 -2
  25. package/docs/quota_dfQuotaTimeoutMs.mjs.html +2 -2
  26. package/docs/quota_fetchQuotaJson.mjs.html +2 -2
  27. package/docs/quota_fromCodexUsageHttp.mjs.html +2 -2
  28. package/docs/quota_getQuotaAntigravity.mjs.html +2 -2
  29. package/docs/quota_getQuotaClaude.mjs.html +2 -2
  30. package/docs/quota_getQuotaCodex.mjs.html +2 -2
  31. package/docs/quota_readJsonOrNull.mjs.html +2 -2
  32. package/docs/quota_toQuotaLabel.mjs.html +2 -2
  33. package/docs/quota_toQuotaResult.mjs.html +2 -2
  34. package/docs/quota_toQuotaScopedLabel.mjs.html +2 -2
  35. package/docs/quota_toQuotaWindow.mjs.html +2 -2
  36. package/docs/readEnvFile.mjs.html +2 -2
  37. package/docs/resolveProviders.mjs.html +2 -2
  38. package/docs/wkf_callAiWithFallback.mjs.html +2 -2
  39. package/docs/wkf_createFileStore.mjs.html +2 -2
  40. package/docs/wkf_createUsageCounter.mjs.html +2 -2
  41. package/docs/wkf_extractJsonLoose.mjs.html +2 -2
  42. package/docs/wkf_noSideEffectPrefix.mjs.html +2 -2
  43. package/docs/wkf_runFanout.mjs.html +2 -2
  44. package/docs/wkf_runFanoutPipeline.mjs.html +2 -2
  45. package/docs/wkf_runRolePipeline.mjs.html +2 -2
  46. package/docs/wkf_salvageTruncatedArray.mjs.html +2 -2
  47. package/g.mjs +2 -2
  48. package/package.json +1 -1
  49. package/src/WDispatchAi.mjs +4 -2
  50. package/src/adapters.mjs +8 -1
  51. package/src/dispatchApiTypesafeSystemone.mjs +375 -0
  52. package/src/getErrorType.mjs +2 -1
  53. package/src/providers.mjs +32 -1
  54. package/test/tools/fakeServerForApiTest.mjs +75 -5
  55. package/test/unit-WDispatchAi.test.mjs +10 -6
  56. package/test/unit-adapters.test.mjs +5 -3
  57. package/test/unit-dispatchAi.test.mjs +1 -1
  58. package/test/unit-dispatchApiTypesafeSystemone.test.mjs +159 -0
  59. package/test/unit-providers.test.mjs +7 -0
  60. package/test/unit-resolveProviders.test.mjs +2 -2
@@ -29,7 +29,7 @@
29
29
  <nav >
30
30
 
31
31
 
32
- <h2><a href="index.html">Home</a></h2><h3>Global</h3><ul><li><a href="global.html#NO_SIDE_EFFECT">NO_SIDE_EFFECT</a></li><li><a href="global.html#WDispatchAi">WDispatchAi</a></li><li><a href="global.html#adapters">adapters</a></li><li><a href="global.html#agyPrint">agyPrint</a></li><li><a href="global.html#attachErrorType">attachErrorType</a></li><li><a href="global.html#budgetFor">budgetFor</a></li><li><a href="global.html#buildChain">buildChain</a></li><li><a href="global.html#buildValidator">buildValidator</a></li><li><a href="global.html#callAiWithFallback">callAiWithFallback</a></li><li><a href="global.html#callOnce">callOnce</a></li><li><a href="global.html#castPintOr">castPintOr</a></li><li><a href="global.html#collectAdditional">collectAdditional</a></li><li><a href="global.html#createFileStore">createFileStore</a></li><li><a href="global.html#createUsageCounter">createUsageCounter</a></li><li><a href="global.html#decodeJwtPayload">decodeJwtPayload</a></li><li><a href="global.html#defaultIntegratePrompt">defaultIntegratePrompt</a></li><li><a href="global.html#dfQuotaTimeoutMs">dfQuotaTimeoutMs</a></li><li><a href="global.html#dfTimeoutMs">dfTimeoutMs</a></li><li><a href="global.html#directWindow">directWindow</a></li><li><a href="global.html#dispatchAi">dispatchAi</a></li><li><a href="global.html#dispatchAiFallback">dispatchAiFallback</a></li><li><a href="global.html#dispatchAiWkf">dispatchAiWkf</a></li><li><a href="global.html#dispatchAntigravity">dispatchAntigravity</a></li><li><a href="global.html#dispatchApiOpenaiCompat">dispatchApiOpenaiCompat</a></li><li><a href="global.html#dispatchApiOpenaiResponses">dispatchApiOpenaiResponses</a></li><li><a href="global.html#dispatchClaude">dispatchClaude</a></li><li><a href="global.html#dispatchCodex">dispatchCodex</a></li><li><a href="global.html#dispatchOpencode">dispatchOpencode</a></li><li><a href="global.html#extractJsonLoose">extractJsonLoose</a></li><li><a href="global.html#extractOutputText">extractOutputText</a></li><li><a href="global.html#fetchQuotaJson">fetchQuotaJson</a></li><li><a href="global.html#firstStr">firstStr</a></li><li><a href="global.html#fromBucket">fromBucket</a></li><li><a href="global.html#fromCodexUsageHttp">fromCodexUsageHttp</a></li><li><a href="global.html#fromLimitItem">fromLimitItem</a></li><li><a href="global.html#fromRpcWindow">fromRpcWindow</a></li><li><a href="global.html#fromTopField">fromTopField</a></li><li><a href="global.html#fromWindow">fromWindow</a></li><li><a href="global.html#getCliArgs">getCliArgs</a></li><li><a href="global.html#getErrorResult">getErrorResult</a></li><li><a href="global.html#getErrorType">getErrorType</a></li><li><a href="global.html#getQuotaAntigravity">getQuotaAntigravity</a></li><li><a href="global.html#getQuotaClaude">getQuotaClaude</a></li><li><a href="global.html#getQuotaCodex">getQuotaCodex</a></li><li><a href="global.html#initState">initState</a></li><li><a href="global.html#isKeyIndependentFail">isKeyIndependentFail</a></li><li><a href="global.html#pickWindow">pickWindow</a></li><li><a href="global.html#readBodyCapped">readBodyCapped</a></li><li><a href="global.html#readEnvFile">readEnvFile</a></li><li><a href="global.html#readJsonOrNull">readJsonOrNull</a></li><li><a href="global.html#redactText">redactText</a></li><li><a href="global.html#reorderByCooling">reorderByCooling</a></li><li><a href="global.html#resolveProviders">resolveProviders</a></li><li><a href="global.html#runFanout">runFanout</a></li><li><a href="global.html#runFanoutPipeline">runFanoutPipeline</a></li><li><a href="global.html#runRolePipeline">runRolePipeline</a></li><li><a href="global.html#salvageTruncatedArray">salvageTruncatedArray</a></li><li><a href="global.html#toQuotaLabel">toQuotaLabel</a></li><li><a href="global.html#toQuotaResult">toQuotaResult</a></li><li><a href="global.html#toQuotaScopedLabel">toQuotaScopedLabel</a></li><li><a href="global.html#toQuotaWindow">toQuotaWindow</a></li><li><a href="global.html#viaAppServer">viaAppServer</a></li><li><a href="global.html#viaHttp">viaHttp</a></li><li><a href="global.html#windowToSeconds">windowToSeconds</a></li></ul>
32
+ <h2><a href="index.html">Home</a></h2><h3>Global</h3><ul><li><a href="global.html#NO_SIDE_EFFECT">NO_SIDE_EFFECT</a></li><li><a href="global.html#WDispatchAi">WDispatchAi</a></li><li><a href="global.html#adapters">adapters</a></li><li><a href="global.html#agyPrint">agyPrint</a></li><li><a href="global.html#attachErrorType">attachErrorType</a></li><li><a href="global.html#budgetFor">budgetFor</a></li><li><a href="global.html#buildChain">buildChain</a></li><li><a href="global.html#buildValidator">buildValidator</a></li><li><a href="global.html#callAiWithFallback">callAiWithFallback</a></li><li><a href="global.html#callOnce">callOnce</a></li><li><a href="global.html#castPintOr">castPintOr</a></li><li><a href="global.html#collectAdditional">collectAdditional</a></li><li><a href="global.html#createFileStore">createFileStore</a></li><li><a href="global.html#createUsageCounter">createUsageCounter</a></li><li><a href="global.html#decodeJwtPayload">decodeJwtPayload</a></li><li><a href="global.html#defaultIntegratePrompt">defaultIntegratePrompt</a></li><li><a href="global.html#dfQuotaTimeoutMs">dfQuotaTimeoutMs</a></li><li><a href="global.html#dfTimeoutMs">dfTimeoutMs</a></li><li><a href="global.html#directWindow">directWindow</a></li><li><a href="global.html#dispatchAi">dispatchAi</a></li><li><a href="global.html#dispatchAiFallback">dispatchAiFallback</a></li><li><a href="global.html#dispatchAiWkf">dispatchAiWkf</a></li><li><a href="global.html#dispatchAntigravity">dispatchAntigravity</a></li><li><a href="global.html#dispatchApiOpenaiCompat">dispatchApiOpenaiCompat</a></li><li><a href="global.html#dispatchApiOpenaiResponses">dispatchApiOpenaiResponses</a></li><li><a href="global.html#dispatchApiTypesafeSystemone">dispatchApiTypesafeSystemone</a></li><li><a href="global.html#dispatchClaude">dispatchClaude</a></li><li><a href="global.html#dispatchCodex">dispatchCodex</a></li><li><a href="global.html#dispatchOpencode">dispatchOpencode</a></li><li><a href="global.html#extractJsonLoose">extractJsonLoose</a></li><li><a href="global.html#extractOutputText">extractOutputText</a></li><li><a href="global.html#fetchQuotaJson">fetchQuotaJson</a></li><li><a href="global.html#firstStr">firstStr</a></li><li><a href="global.html#fromBucket">fromBucket</a></li><li><a href="global.html#fromCodexUsageHttp">fromCodexUsageHttp</a></li><li><a href="global.html#fromLimitItem">fromLimitItem</a></li><li><a href="global.html#fromRpcWindow">fromRpcWindow</a></li><li><a href="global.html#fromTopField">fromTopField</a></li><li><a href="global.html#fromWindow">fromWindow</a></li><li><a href="global.html#getCliArgs">getCliArgs</a></li><li><a href="global.html#getErrorResult">getErrorResult</a></li><li><a href="global.html#getErrorType">getErrorType</a></li><li><a href="global.html#getQuotaAntigravity">getQuotaAntigravity</a></li><li><a href="global.html#getQuotaClaude">getQuotaClaude</a></li><li><a href="global.html#getQuotaCodex">getQuotaCodex</a></li><li><a href="global.html#initState">initState</a></li><li><a href="global.html#isKeyIndependentFail">isKeyIndependentFail</a></li><li><a href="global.html#pickWindow">pickWindow</a></li><li><a href="global.html#readBodyCapped">readBodyCapped</a></li><li><a href="global.html#readEnvFile">readEnvFile</a></li><li><a href="global.html#readJsonOrNull">readJsonOrNull</a></li><li><a href="global.html#redactText">redactText</a></li><li><a href="global.html#reorderByCooling">reorderByCooling</a></li><li><a href="global.html#resolveProviders">resolveProviders</a></li><li><a href="global.html#runFanout">runFanout</a></li><li><a href="global.html#runFanoutPipeline">runFanoutPipeline</a></li><li><a href="global.html#runRolePipeline">runRolePipeline</a></li><li><a href="global.html#salvageTruncatedArray">salvageTruncatedArray</a></li><li><a href="global.html#toQuotaLabel">toQuotaLabel</a></li><li><a href="global.html#toQuotaResult">toQuotaResult</a></li><li><a href="global.html#toQuotaScopedLabel">toQuotaScopedLabel</a></li><li><a href="global.html#toQuotaWindow">toQuotaWindow</a></li><li><a href="global.html#viaAppServer">viaAppServer</a></li><li><a href="global.html#viaHttp">viaHttp</a></li><li><a href="global.html#windowToSeconds">windowToSeconds</a></li></ul>
33
33
 
34
34
  </nav>
35
35
 
@@ -154,7 +154,7 @@ export default salvageTruncatedArray
154
154
  <br class="clear">
155
155
 
156
156
  <footer>
157
- Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Mon Sep 14 2026 12:59:17 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
157
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Thu Sep 17 2026 13:48:00 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
158
158
  </footer>
159
159
 
160
160
  <script>prettyPrint();</script>
package/g.mjs CHANGED
@@ -27,7 +27,7 @@ let test = async () => {
27
27
 
28
28
  //可用之AI供應商種類
29
29
  console.log('KINDS:', wdi.KINDS)
30
- // => KINDS: [ 'opencode', 'claude', 'codex', 'antigravity', 'api-openai-compat', 'api-openai-responses' ]
30
+ // => KINDS: [ 'opencode', 'claude', 'codex', 'antigravity', 'api-openai-compat', 'api-openai-responses', 'api-typesafe-systemone' ]
31
31
 
32
32
  let prompt = '請只回覆兩個字:完成,不要有任何其他文字'
33
33
 
@@ -84,7 +84,7 @@ let test = async () => {
84
84
  //未知供應商回傳error結果物件, 不會reject
85
85
  let r4 = await wdi.dispatchAi('gemini', prompt)
86
86
  console.log('invalid kind:', r4.ok, r4.error)
87
- // => invalid kind: false unknown ai kind: "gemini" (available: opencode, claude, codex, antigravity, api-openai-compat, api-openai-responses)
87
+ // => invalid kind: false unknown ai kind: "gemini" (available: opencode, claude, codex, antigravity, api-openai-compat, api-openai-responses, api-typesafe-systemone)
88
88
 
89
89
  //prompt非有效字串亦回傳error結果物件
90
90
  let r5 = await wdi.dispatchClaude('')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "w-dispatch-ai",
3
- "version": "1.0.25",
3
+ "version": "1.0.26",
4
4
  "main": "dist/w-dispatch-ai.umd.js",
5
5
  "dependencies": {
6
6
  "wsemi": "^1.8.92"
@@ -9,6 +9,7 @@ import dispatchCodex from './dispatchCodex.mjs'
9
9
  import dispatchAntigravity from './dispatchAntigravity.mjs'
10
10
  import dispatchApiOpenaiCompat from './dispatchApiOpenaiCompat.mjs'
11
11
  import dispatchApiOpenaiResponses from './dispatchApiOpenaiResponses.mjs'
12
+ import dispatchApiTypesafeSystemone from './dispatchApiTypesafeSystemone.mjs'
12
13
  import providers from './providers.mjs'
13
14
  import resolveProviders from './resolveProviders.mjs'
14
15
  import readEnvFile from './readEnvFile.mjs'
@@ -37,10 +38,10 @@ let KINDS = keys(adapters)
37
38
  /**
38
39
  * AI供應商分派
39
40
  *
40
- * @returns {Object} 回傳物件,其內含KINDS(可用供應商種類字串陣列),dispatchAiWkf之工作流工廠函數,dispatchAi、dispatchAiFallback、dispatchOpencode、dispatchClaude、dispatchCodex、dispatchAntigravity、dispatchApiOpenaiCompat、dispatchApiOpenaiResponses之async函數,providers(預設providers定義檔)與resolveProviders(envVar展開器),以及readEnvFile(讀.env不污染process.env)、budgetFor(遞補鏈時間預算推導)、createFileStore(store檔案持久化)、createUsageCounter(逐日用量計帳)、salvageTruncatedArray(截斷陣列搶救)、NO_SIDE_EFFECT(防副作用prompt前綴)等工具,以及getQuotaClaude、getQuotaCodex、getQuotaAntigravity(查詢各訂閱制CLI本機登入帳號之額度窗口,統一結構,可比對指定email)
41
+ * @returns {Object} 回傳物件,其內含KINDS(可用供應商種類字串陣列),dispatchAiWkf之工作流工廠函數,dispatchAi、dispatchAiFallback、dispatchOpencode、dispatchClaude、dispatchCodex、dispatchAntigravity、dispatchApiOpenaiCompat、dispatchApiOpenaiResponses、dispatchApiTypesafeSystemone(TypeSafe jev之型別化決策API,非文字生成)之async函數,providers(預設providers定義檔)與resolveProviders(envVar展開器),以及readEnvFile(讀.env不污染process.env)、budgetFor(遞補鏈時間預算推導)、createFileStore(store檔案持久化)、createUsageCounter(逐日用量計帳)、salvageTruncatedArray(截斷陣列搶救)、NO_SIDE_EFFECT(防副作用prompt前綴)等工具,以及getQuotaClaude、getQuotaCodex、getQuotaAntigravity(查詢各訂閱制CLI本機登入帳號之額度窗口,統一結構,可比對指定email)
41
42
  * @example
42
43
  *
43
- * 詳見dispatchAi、dispatchAiFallback、dispatchAiWkf、dispatchOpencode、dispatchClaude、dispatchCodex、dispatchAntigravity、dispatchApiOpenaiCompat、dispatchApiOpenaiResponses、resolveProviders、getQuotaClaude、getQuotaCodex、getQuotaAntigravity範例
44
+ * 詳見dispatchAi、dispatchAiFallback、dispatchAiWkf、dispatchOpencode、dispatchClaude、dispatchCodex、dispatchAntigravity、dispatchApiOpenaiCompat、dispatchApiOpenaiResponses、dispatchApiTypesafeSystemone、resolveProviders、getQuotaClaude、getQuotaCodex、getQuotaAntigravity範例
44
45
  *
45
46
  */
46
47
  let WDispatchAi = {
@@ -55,6 +56,7 @@ let WDispatchAi = {
55
56
  dispatchAntigravity,
56
57
  dispatchApiOpenaiCompat,
57
58
  dispatchApiOpenaiResponses,
59
+ dispatchApiTypesafeSystemone,
58
60
  providers,
59
61
  resolveProviders,
60
62
  readEnvFile,
package/src/adapters.mjs CHANGED
@@ -4,6 +4,7 @@ import dispatchCodex from './dispatchCodex.mjs'
4
4
  import dispatchAntigravity from './dispatchAntigravity.mjs'
5
5
  import dispatchApiOpenaiCompat from './dispatchApiOpenaiCompat.mjs'
6
6
  import dispatchApiOpenaiResponses from './dispatchApiOpenaiResponses.mjs'
7
+ import dispatchApiTypesafeSystemone from './dispatchApiTypesafeSystemone.mjs'
7
8
 
8
9
 
9
10
  // adapters.mjs — kind對照表, 亦為「CLI或API」之選型判準所在
@@ -38,6 +39,11 @@ import dispatchApiOpenaiResponses from './dispatchApiOpenaiResponses.mjs'
38
39
  // 同一條dispatchAiFallback鏈可逐條目混搭kind, 工作流各階段亦然:
39
40
  // 產生候選、整合收斂等純文字階段用API(快且省), 需要讀專案檔案或grep的
40
41
  // 階段換CLI。判準永遠是「這一步需不需要碰外部世界」, 而非整條鏈二選一。
42
+ //
43
+ // ══ 例外: api-typesafe-systemone 不是文字生成 ══
44
+ //
45
+ // TypeSafe之jev(System One模型)對prompt(state)回答型別化questions, stdout為答案JSON,
46
+ // 與上述文字生成kind之輸出形狀不同, 不可與其混在同一條遞補鏈; 詳見dispatchApiTypesafeSystemone.mjs檔頭。
41
47
 
42
48
 
43
49
  /**
@@ -52,7 +58,7 @@ import dispatchApiOpenaiResponses from './dispatchApiOpenaiResponses.mjs'
52
58
  * import adapters from './src/adapters.mjs'
53
59
  *
54
60
  * console.log(Object.keys(adapters))
55
- * // => ['opencode', 'claude', 'codex', 'antigravity', 'api-openai-compat', 'api-openai-responses']
61
+ * // => ['opencode', 'claude', 'codex', 'antigravity', 'api-openai-compat', 'api-openai-responses', 'api-typesafe-systemone']
56
62
  *
57
63
  */
58
64
  let adapters = {
@@ -62,6 +68,7 @@ let adapters = {
62
68
  'antigravity': dispatchAntigravity,
63
69
  'api-openai-compat': dispatchApiOpenaiCompat,
64
70
  'api-openai-responses': dispatchApiOpenaiResponses,
71
+ 'api-typesafe-systemone': dispatchApiTypesafeSystemone,
65
72
  }
66
73
 
67
74
 
@@ -0,0 +1,375 @@
1
+ import get from 'lodash-es/get.js'
2
+ import isobj from 'wsemi/src/isobj.mjs'
3
+ import iseobj from 'wsemi/src/iseobj.mjs'
4
+ import isnum from 'wsemi/src/isnum.mjs'
5
+ import cint from 'wsemi/src/cint.mjs'
6
+ import isestr from 'wsemi/src/isestr.mjs'
7
+ import isp0int from 'wsemi/src/isp0int.mjs'
8
+ import delay from 'wsemi/src/delay.mjs'
9
+ import strTruncate from 'wsemi/src/strTruncate.mjs'
10
+ import castPintOr from './castPintOr.mjs'
11
+ import buildValidator from './buildValidator.mjs'
12
+ import getErrorResult from './getErrorResult.mjs'
13
+ import dfTimeoutMs from './dfTimeoutMs.mjs'
14
+
15
+
16
+ // dispatchApiTypesafeSystemone.mjs — 以fetch直呼TypeSafe AI之System One API(POST /v1/systemone)
17
+ //
18
+ // 【這不是文字生成模型(2026-09-17實測後新增)】TypeSafe之jev為「System One」決策模型:
19
+ // 不產生文字, 而是對一段state(被評估的內容)回答呼叫端定義的型別化問題(questions),
20
+ // 每題回傳受限於呼叫端選項之答案與機率。權威文件: https://docs.typesafe.ai/api
21
+ // (索引 https://docs.typesafe.ai/llms.txt)。官方僅提供Python/JavaScript SDK、Playground與
22
+ // agent skill, 無CLI(npm之@typesafe-ai/sdk無bin、PyPI之typesafe-sdk無console script), 故只有API版。
23
+ //
24
+ // 【請求】POST <baseURL>/systemone, Authorization: Bearer <key>, body為{ model, state, questions }:
25
+ // state ← 本轉接器之prompt(字串)。結構化內容請以JSON.stringify(物件)傳入——2026-09-17實測
26
+ // 同一組引用欄位路徑之問題, 物件state與JSON字串state之答案一致(差異≤0.01)。
27
+ // questions ← opt.questions(必填, 非空物件), 鍵為呼叫端自訂之題目id, 值為:
28
+ // { type:'noul', instructions, criteria?:{true,false} } 是非題, 回yes之機率
29
+ // { type:'choice', instructions, criteria:{選項:描述|null} } 單選題
30
+ // { type:'score', instructions, criteria:[層級描述...] } 有序量表(至少2級)
31
+ // 題型與欄位由伺服器驗證(不合規回422), 本轉接器只擋「未給questions」, 不重複伺服器規則。
32
+ // model ← opt.model, 預設'jev-latest'(官方SDK之預設; 回應之model為實際版本如'jev-1.13.0')。
33
+ //
34
+ // 【回應(實測)】{ model:'jev-1.13.0', answers:{ <id>:{type,choice|score|noul,probabilities?,confidence?,legend?} },
35
+ // usage:{ input_tokens, output_tokens } }; 回應標頭帶x-typesafe-request-id。
36
+ // 本轉接器之stdout為JSON.stringify(answers)(令dispatchAiFallback與工作流層之parse/check無須修改即可用),
37
+ // 另於結果物件追加answers(已解析物件)與modelResolved(實際模型版本)。
38
+ // 請求的題目id在answers中缺任一個即視為INVALID_RESPONSE, 不讓下游取到undefined。
39
+ //
40
+ // 【錯誤(實測)】壞金鑰401 {detail:{error_type:'authentication_error'}}; 未知model 400
41
+ // {detail:{error_type:'api_usage_error', message:'Unknown model: X'}}; 題型不合規或questions為空422
42
+ // {detail:[{type,loc,msg,...}]}。皆以HTTP <code>回報、原始本體置stderr; 4xx(429除外)不重試。
43
+ //
44
+ // 【混用注意】
45
+ // 1. 答案形狀與文字模型完全不同, 不可與文字生成條目混在同一條dispatchAiFallback鏈中遞補;
46
+ // 同一鏈只放本kind之條目(可多把金鑰輪替)。預設providers.mjs收有typesafe:jev-latest,
47
+ // 請以pick單獨取出; 全取做文字遞補時該條因無questions以params錯誤0ms失敗後換下一家。
48
+ // 2. questions於dispatchAiFallback可放呼叫層opt(非fallback自用鍵會透傳給各條目), 不必寫進條目。
49
+ // 3. 工作流callAi預設在prompt前掛防寫檔前綴(NO_SIDE_EFFECT), 對本kind等於把前綴混進被評估的state,
50
+ // 經工作流呼叫時務必傳promptPrefix:''。
51
+
52
+
53
+ //預設值
54
+ let DEFAULT_BASE_URL = 'https://api.typesafe.ai/v1'
55
+ let DEFAULT_MODEL = 'jev-latest'
56
+ let DEFAULT_TIMEOUT_MS = dfTimeoutMs //全套件統一預設300000
57
+ let DEFAULT_RETRY_DELAY_MS = 5000
58
+ let MAX_RETRY_DELAY_MS = 15000
59
+
60
+
61
+ //optTruncate, 裁切失敗結果之內容時於刪節號後標註原始總長度(同execCli)
62
+ let optTruncate = {
63
+ funWithMsg: (str) => `(truncated, total ${str.length} chars)`,
64
+ }
65
+
66
+
67
+ /**
68
+ * 單次HTTP呼叫(內部使用, 不含重試邏輯)
69
+ *
70
+ * @param {String} url 輸入完整端點網址字串
71
+ * @param {Object} headers 輸入請求標頭物件
72
+ * @param {Object} body 輸入請求本體物件
73
+ * @param {Array} ids 輸入請求之題目id陣列,用於檢核回應是否每題皆有答案
74
+ * @param {Number} timeoutMs 輸入逾時毫秒
75
+ * @param {Function|null} validator 輸入驗證函式
76
+ * @returns {Promise} 回傳Promise,resolve回傳結果物件
77
+ */
78
+ async function callOnce(url, headers, body, ids, timeoutMs, validator) {
79
+
80
+ let t0 = Date.now()
81
+
82
+ //mkResult, 結果形狀之單一來源(欄位對齊其他api類轉接器, 追加answers與modelResolved)
83
+ let mkResult = (patch) => ({
84
+ ok: false,
85
+ stdout: '',
86
+ stderr: '',
87
+ code: null,
88
+ error: '',
89
+ durationMs: Date.now() - t0,
90
+ usage: null,
91
+ answers: null,
92
+ modelResolved: '',
93
+ ...patch,
94
+ })
95
+
96
+ //AbortController, 逾時中止(含回應本體之讀取)
97
+ let controller = new AbortController()
98
+ let timer = setTimeout(() => {
99
+ controller.abort()
100
+ }, timeoutMs)
101
+
102
+ let res = null
103
+ let txt = ''
104
+ try {
105
+ res = await fetch(url, {
106
+ method: 'POST',
107
+ headers,
108
+ body: JSON.stringify(body),
109
+ signal: controller.signal,
110
+ })
111
+ txt = await res.text()
112
+ }
113
+ catch (err) {
114
+ clearTimeout(timer)
115
+
116
+ //逾時, error以TIMEOUT開頭令dispatchAiFallback視為與金鑰無關而跳組
117
+ if (err.name === 'AbortError') {
118
+ return mkResult({ error: `TIMEOUT after ${timeoutMs / 1000}s`, errorType: 'timeout' })
119
+ }
120
+
121
+ //網路層錯誤(DNS/連線拒絕等)
122
+ let cause = get(err, 'cause.code', '') || err.message
123
+ return mkResult({ error: `FETCH_ERROR: ${cause}`, errorType: 'fetch' })
124
+ }
125
+ clearTimeout(timer)
126
+
127
+ //HTTP非2xx, 原始回應本體放stderr(401/400之detail.message、422之detail陣列皆在其中)
128
+ if (!res.ok) {
129
+ return mkResult({
130
+ stderr: strTruncate(txt, 1000, optTruncate),
131
+ code: res.status,
132
+ error: `HTTP ${res.status}`,
133
+ errorType: 'http',
134
+ })
135
+ }
136
+
137
+ //解析answers/model/usage
138
+ let answers = null
139
+ let modelResolved = ''
140
+ let usage = null
141
+ try {
142
+ let j = JSON.parse(txt)
143
+ answers = get(j, 'answers', null)
144
+ modelResolved = get(j, 'model', '')
145
+ if (!isestr(modelResolved)) {
146
+ modelResolved = ''
147
+ }
148
+ usage = get(j, 'usage', null)
149
+ if (!isobj(usage)) {
150
+ usage = null
151
+ }
152
+ }
153
+ catch {
154
+ answers = null
155
+ }
156
+ if (!isobj(answers)) {
157
+ return mkResult({
158
+ stderr: strTruncate(txt, 500, optTruncate),
159
+ code: res.status,
160
+ error: 'INVALID_RESPONSE: missing answers object',
161
+ errorType: 'invalid-response',
162
+ usage,
163
+ modelResolved,
164
+ })
165
+ }
166
+
167
+ //每個請求之題目皆須有答案, 缺漏即不合規(避免下游取到undefined)
168
+ let missing = ids.filter((id) => !isobj(answers[id]))
169
+ if (missing.length > 0) {
170
+ return mkResult({
171
+ stderr: strTruncate(txt, 500, optTruncate),
172
+ code: res.status,
173
+ error: `INVALID_RESPONSE: missing answers for [${missing.join(', ')}]`,
174
+ errorType: 'invalid-response',
175
+ usage,
176
+ modelResolved,
177
+ })
178
+ }
179
+
180
+ //content, answers序列化為stdout(令遞補層與工作流層之parse/check通用)
181
+ let content = JSON.stringify(answers)
182
+
183
+ //validator, error與execCli一致令dispatchAiFallback可統一分流
184
+ if (validator && !validator(content)) {
185
+ return mkResult({
186
+ stdout: strTruncate(content, 500, optTruncate),
187
+ code: res.status,
188
+ error: 'OUTPUT_VALIDATION_FAILED',
189
+ errorType: 'validation',
190
+ usage,
191
+ modelResolved,
192
+ })
193
+ }
194
+
195
+ return mkResult({
196
+ ok: true,
197
+ stdout: content,
198
+ code: res.status,
199
+ usage,
200
+ answers,
201
+ modelResolved,
202
+ })
203
+ }
204
+
205
+
206
+ //本轉接器不使用execCli, 全部設定鍵自理, 未知鍵一律忽略
207
+
208
+
209
+ /**
210
+ * 以fetch直呼TypeSafe AI之System One API(POST /v1/systemone),對prompt(即state)回答型別化問題
211
+ *
212
+ * 特點:
213
+ * 非文字生成——jev等System One模型對state回答呼叫端定義之questions(noul是非題、choice單選題、score量表),
214
+ * 每題回傳受限於選項之答案與機率;官方無CLI,僅此API路徑;
215
+ * prompt即state,結構化內容以JSON.stringify(物件)傳入(實測答案與物件state一致);
216
+ * stdout為answers之JSON字串,另於結果追加answers(已解析物件)與modelResolved(實際模型版本);
217
+ * 請求之題目id於回應缺任一即以INVALID_RESPONSE回報;
218
+ * 不可與文字生成條目混在同一條遞補鏈,經工作流callAi呼叫時須傳promptPrefix:''(理由見檔頭);
219
+ * 本函數不會reject,一律以結果物件之ok與error欄位回報成敗
220
+ *
221
+ * @param {String} prompt 輸入被評估之內容字串,作為state置於HTTP body,結構化內容請先JSON.stringify
222
+ * @param {Object} [opt={}] 輸入設定物件,預設{}
223
+ * @param {Object} opt.questions 輸入題目物件(必填且非空),鍵為自訂題目id,值為{type:'noul'|'choice'|'score', instructions, criteria},格式見檔頭與https://docs.typesafe.ai/api
224
+ * @param {String} [opt.baseURL='https://api.typesafe.ai/v1'] 輸入API基底網址字串,將於尾端接上/systemone,預設官方端點
225
+ * @param {String} [opt.model='jev-latest'] 輸入模型名稱字串,預設'jev-latest'(另有'jev-preview')
226
+ * @param {String} [opt.key=''] 輸入API key字串,以Bearer置於Authorization標頭,預設''代表不帶認證標頭
227
+ * @param {Object} [opt.body={}] 輸入額外請求本體物件,將併入預設body(同名鍵以此為準),預設{}
228
+ * @param {Object} [opt.headers={}] 輸入額外請求標頭物件,預設{}
229
+ * @param {Number} [opt.timeoutMs=300000] 輸入逾時毫秒正整數,逾時將中止請求,全套件統一預設300000
230
+ * @param {String|Function} [opt.validate=undefined] 輸入stdout(answers之JSON字串)驗證規則字串或自訂驗證函數,規則字串支援'nonempty'、'json'、'min:100',多規則可用逗號串接,預設undefined代表不驗證
231
+ * @param {Number} [opt.maxRetries=0] 輸入失敗後最大重試次數非負整數,4xx(429除外)不重試,預設0
232
+ * @param {Number} [opt.retryDelayMs=5000] 輸入重試間隔毫秒正整數,實際間隔為retryDelayMs乘以重試次數且上限15000ms,預設5000
233
+ * @returns {Promise} 回傳Promise,resolve回傳結果物件,內含ok(是否成功布林值)、stdout(answers之JSON字串)、stderr(失敗時之原始回應本體)、code(HTTP狀態碼,網路錯誤與逾時為null)、error(錯誤訊息字串,成功時為空字串)、errorType(僅失敗時,機器可讀錯誤類別字串,一覽見getErrorType.mjs檔頭)、durationMs(耗時毫秒)、attempts(實際嘗試次數)、usage(原始回應之token用量物件原樣透傳,欄位名為input_tokens/output_tokens,無則null)、answers(成功時為已解析之答案物件,否則null)、modelResolved(回應所載之實際模型版本字串,如'jev-1.13.0',無則''),本函數不會reject
234
+ * @example
235
+ * //need network and a TypeSafe api key, no cli required
236
+ *
237
+ * import dispatchApiTypesafeSystemone from './src/dispatchApiTypesafeSystemone.mjs'
238
+ *
239
+ * let test = async () => {
240
+ *
241
+ * let r = await dispatchApiTypesafeSystemone('房間浴室水龍頭一直滴水,吵到睡不著', {
242
+ * key: 'apikey_xxxxxx',
243
+ * questions: {
244
+ * category: {
245
+ * type: 'choice',
246
+ * instructions: '這則客房訊息屬於哪一類?',
247
+ * criteria: {
248
+ * '設備故障報修': '客人回報房間硬體設備損壞、水電問題或故障',
249
+ * '索取備品': '客人需要毛巾、牙刷、礦泉水等客房備品',
250
+ * '其他': null,
251
+ * },
252
+ * },
253
+ * urgent: { type: 'noul', instructions: '客人是否表達急迫性?' },
254
+ * },
255
+ * })
256
+ * console.log(r.ok, r.answers.category.choice, r.answers.urgent.noul)
257
+ * // => true 設備故障報修 0.86 (2026-09-17實測; noul為機率, 每次可能差0.01)
258
+ *
259
+ * let re = await dispatchApiTypesafeSystemone('abc', { key: 'apikey_bad', questions: { a: { type: 'noul', instructions: 'x?' } } })
260
+ * console.log(re.ok, re.code, re.errorType)
261
+ * // => false 401 http
262
+ *
263
+ * }
264
+ * await test()
265
+ * .catch((err) => {
266
+ * console.log(err)
267
+ * })
268
+ *
269
+ */
270
+ async function dispatchApiTypesafeSystemone(prompt, opt = {}) {
271
+
272
+ //check prompt, 不reject故以錯誤結果物件回報
273
+ if (!isestr(prompt)) {
274
+ return getErrorResult('prompt must be a non-empty string')
275
+ }
276
+
277
+ //questions必填且非空, 題型細節交伺服器驗證(回422)
278
+ let questions = get(opt, 'questions', null)
279
+ if (!iseobj(questions)) {
280
+ return getErrorResult('questions must be a non-empty object')
281
+ }
282
+
283
+ //baseURL, 無效回退官方端點
284
+ let baseURL = get(opt, 'baseURL', null)
285
+ if (!isestr(baseURL)) {
286
+ baseURL = DEFAULT_BASE_URL
287
+ }
288
+
289
+ //model, 無效回退官方預設
290
+ let model = get(opt, 'model', null)
291
+ if (!isestr(model)) {
292
+ model = DEFAULT_MODEL
293
+ }
294
+
295
+ //key, 無效代表不帶認證標頭
296
+ let key = get(opt, 'key', null)
297
+
298
+ //bodyExtra
299
+ let bodyExtra = get(opt, 'body', null)
300
+ if (!isobj(bodyExtra)) {
301
+ bodyExtra = {}
302
+ }
303
+
304
+ //headersExtra
305
+ let headersExtra = get(opt, 'headers', null)
306
+ if (!isobj(headersExtra)) {
307
+ headersExtra = {}
308
+ }
309
+
310
+ //timeoutMs
311
+ let timeoutMs = castPintOr(get(opt, 'timeoutMs', null), DEFAULT_TIMEOUT_MS)
312
+
313
+ //maxRetries
314
+ let maxRetries = get(opt, 'maxRetries', null)
315
+ if (!isp0int(maxRetries)) {
316
+ maxRetries = 0
317
+ }
318
+ else {
319
+ maxRetries = cint(maxRetries)
320
+ }
321
+
322
+ //retryDelayMs
323
+ let retryDelayMs = castPintOr(get(opt, 'retryDelayMs', null), DEFAULT_RETRY_DELAY_MS)
324
+
325
+ //validator
326
+ let validator = buildValidator(get(opt, 'validate', null))
327
+
328
+ //url, baseURL尾端斜線正規化後接上端點
329
+ let url = baseURL.replace(/\/+$/, '') + '/systemone'
330
+
331
+ //body, prompt即state; 額外鍵以bodyExtra為準
332
+ let body = { model, state: prompt, questions, ...bodyExtra }
333
+
334
+ //ids, 用於檢核回應是否每題皆有答案(以實際送出之questions為準)
335
+ let ids = Object.keys(isobj(body.questions) ? body.questions : questions)
336
+
337
+ //headers
338
+ let headers = { 'Content-Type': 'application/json', ...headersExtra }
339
+ if (isestr(key)) {
340
+ headers['Authorization'] = `Bearer ${key}`
341
+ }
342
+
343
+ let lastResult = null
344
+ let totalAttempts = 0
345
+
346
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
347
+
348
+ //delay, 重試間隔隨次數遞增, 上限15000ms(同execCli)
349
+ if (attempt > 0) {
350
+ await delay(Math.min(retryDelayMs * attempt, MAX_RETRY_DELAY_MS))
351
+ }
352
+
353
+ lastResult = await callOnce(url, headers, body, ids, timeoutMs, validator)
354
+ totalAttempts = attempt + 1
355
+
356
+ if (lastResult.ok) {
357
+ lastResult.attempts = totalAttempts
358
+ return lastResult
359
+ }
360
+
361
+ //不可重試: 4xx(429除外)為客戶端錯誤, 重試無意義
362
+ let c = lastResult.code
363
+ if (isnum(c) && c >= 400 && c < 500 && c !== 429) {
364
+ break
365
+ }
366
+
367
+ }
368
+
369
+ lastResult.attempts = totalAttempts
370
+
371
+ return lastResult
372
+ }
373
+
374
+
375
+ export default dispatchApiTypesafeSystemone
@@ -22,7 +22,8 @@ import isestr from 'wsemi/src/isestr.mjs'
22
22
  // 'http' HTTP非2xx(code為狀態碼, 僅api類)
23
23
  // 'fetch' 網路層錯誤(DNS/連線拒絕, 僅api類)
24
24
  // 'tool-unsupported' 模型回tool_calls而本轉接器不支援工具(僅api類)
25
- // 'invalid-response' 回應結構不合規(缺choices[0].message.content或缺output陣列, 僅api類)
25
+ // 'invalid-response' 回應結構不合規(缺choices[0].message.content、缺output陣列,
26
+ // 或api-typesafe-systemone缺answers物件/缺所請求題目之答案, 僅api類)
26
27
  // 'incomplete' Responses API之status非completed(如max_output_tokens耗盡而截斷,
27
28
  // 僅api-openai-responses; 半截內容不當成功回傳, 見該轉接器檔頭)
28
29
  // 'aborted' shouldStop中止(僅dispatchAiFallback)
package/src/providers.mjs CHANGED
@@ -7,6 +7,7 @@
7
7
  // 【id命名】依「路徑:模型」規則(詳dispatchAiFallback.mjs檔頭之id設計規則):
8
8
  // oc:=經opencode CLI(有工具、較慢) / agy:=antigravity CLI / claude:/codex:=訂閱登入態CLI
9
9
  // zen:/agnes:/poolside:=REST直呼(免CLI免登入、快, 純文字生成)
10
+ // typesafe:=REST直呼之決策模型(非文字生成, 須給questions, 見該條註解)
10
11
  // 同一模型之CLI版與REST版屬不同供應商(能力與額度池皆不同), 故各為一條。
11
12
  //
12
13
  // ══════════════════════════════════════════════════════════════════════════════
@@ -51,6 +52,9 @@
51
52
  // HTTP 503 → 上游時斷時續(server_error: Upstream request failed: Endpoint is unavailable),
52
53
  // 暫時性, 遞補層換家即可; 判別法: 同時段打另一免費模型作對照組。
53
54
  // HTTP 429 → 閘道容量型限流(FreeUsageLimitError), 與金鑰無關(換金鑰照樣429), 暫時性。
55
+ // HTTP 403 FreeTierError(free tier can only be used from within OpenCode)
56
+ // → 該免費模型只開放opencode客戶端, REST直呼被刻意擋下(2026-09-17 union-alpha實測), 屬政策非故障,
57
+ // 只能收oc:版。另401 Model is disabled=該金鑰所屬工作區未開此模型, 換工作區或於後台開啟。
54
58
  // HTTP 401/400 → 額度/促銷結束或模型暫時下架(訊息如Free promotion has ended、
55
59
  // Upstream request failed: Model is unavailable), 屬服務端狀態非設定錯誤。
56
60
  // 任一碼皆非「條目寫錯」之證據——model id寫錯時閘道回的是其他4xx且訊息明指model。
@@ -80,9 +84,11 @@
80
84
  // import providers from 'w-dispatch-ai/src/providers.mjs'
81
85
  // import resolveProviders from 'w-dispatch-ai/src/resolveProviders.mjs'
82
86
  // import readEnvFile from 'w-dispatch-ai/src/readEnvFile.mjs'
83
- // let env = readEnvFile('./.env') //OPENCODE_KEYS/AGNES_KEYS/POOLSIDE_KEYS, 逗號分隔多把; 不污染process.env
87
+ // let env = readEnvFile('./.env') //OPENCODE_KEYS/AGNES_KEYS/POOLSIDE_KEYS/TYPESAFE_KEYS, 逗號分隔多把; 不污染process.env
84
88
  // let { providers: ps, table, skipped } = resolveProviders(providers, { env }) //全取
85
89
  // let r2 = resolveProviders(providers, { env, pick: ['agnes:agnes-3.0-flash', 'claude:sonnet'] }) //自選, pick順序即遞補優先序
90
+ // let r3 = resolveProviders(providers, { env, pick: ['typesafe:jev-latest'] }) //決策模型單獨取出
91
+ // await dispatchAiFallback(state, { providers: r3.providers, questions }) //questions置呼叫層
86
92
  //
87
93
  // 【timeout規劃】各條目刻意不帶timeoutMs, 由上層依任務型態統一給定、條目僅於特例覆寫:
88
94
  // 簡單任務(秒級~分鐘級): 沿用套件統一預設即可(全轉接器一律300000=5分鐘, 見dfTimeoutMs.mjs)。
@@ -162,6 +168,20 @@ let providers = [
162
168
  //2026-09-03實測6.1s(opencode CLI 1.18.27)。註: 同日同模型走zen REST回HTTP 500,
163
169
  //僅CLI路徑可用——同模型不同路徑屬不同供應商之實例(故未增zen:對應條目)
164
170
  },
171
+ {
172
+ id: 'oc:opencode/union-alpha',
173
+ model: 'opencode/union-alpha',
174
+ kind: 'opencode',
175
+ provider: 'opencode',
176
+ config: {
177
+ permission: { edit: 'deny', write: 'deny', bash: 'deny' },
178
+ },
179
+ //Union Alpha Free(官方文件: 限時免費之stealth模型, 端點/messages)。2026-09-17實測(opencode CLI 1.18.31):
180
+ //刻意不帶envVar——不注入金鑰時opencode走自身免費存取, 實測6.8s成功; OPENCODE_KEYS之第1把金鑰
181
+ //所屬工作區未開此模型(回Model is disabled), 第2把雖可用但該工作區會預設開啟新模型(可能非免費), 不採用。
182
+ //前提是本機opencode未登入(無auth.json); 若日後以金鑰登入, 須於該工作區開啟此模型。
183
+ //REST不收zen:版: /messages對免費模型回403 FreeTierError(free tier can only be used from within OpenCode)
184
+ },
165
185
  {
166
186
  id: 'oc:opencode/deepseek-v4-flash-free',
167
187
  model: 'opencode/deepseek-v4-flash-free',
@@ -198,6 +218,17 @@ let providers = [
198
218
  },
199
219
 
200
220
  //api版
221
+ {
222
+ id: 'typesafe:jev-latest',
223
+ model: 'jev-latest',
224
+ kind: 'api-typesafe-systemone',
225
+ envVar: 'TYPESAFE_KEYS',
226
+ //TypeSafe之jev為決策模型(非文字生成): prompt為被評估之state, 呼叫時須給questions, 回型別化答案。
227
+ //2026-09-17實測0.3~1.0s(實際版本jev-1.13.0); 官方無CLI故僅此API條目。用法: 以pick單獨取出,
228
+ //questions置dispatchAiFallback呼叫層(詳dispatchApiTypesafeSystemone.mjs檔頭)。
229
+ //混入文字遞補鏈(全取)時: 因無questions而以params錯誤0ms失敗(逐把金鑰各一次)後換下一家, 不影響他家(2026-09-17實測);
230
+ //刻意不放清單末端——前面全敗時fallback回傳最後一筆失敗, 放末端會以「questions必填」掩蓋真正原因
231
+ },
201
232
  //zen:系為2026-08-21快照(檔頭聲明), 各條註記當日以「請只回覆兩個字:完成」實測之結果
202
233
  {
203
234
  id: 'agnes:agnes-3.0-flash',