@cjhyy/code-shell-core 0.5.0-rc.0 → 0.5.0-rc.2

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 (283) hide show
  1. package/dist/agent/agent-definition-registry.d.ts +29 -0
  2. package/dist/agent/agent-definition-registry.js +74 -0
  3. package/dist/agent/agent-definition.d.ts +37 -0
  4. package/dist/agent/agent-definition.js +59 -0
  5. package/dist/arena/arena.js +11 -12
  6. package/dist/arena/context/context-tools.js +2 -4
  7. package/dist/arena/context/within-root.d.ts +7 -0
  8. package/dist/arena/context/within-root.js +15 -0
  9. package/dist/arena/iterate/phases/argue.js +8 -2
  10. package/dist/arena/iterate/phases/revise.js +1 -1
  11. package/dist/arena/iterate/phases/tournament.js +3 -3
  12. package/dist/arena/phases/adjudication.js +1 -4
  13. package/dist/arena/phases/build-consensus.js +1 -4
  14. package/dist/arena/phases/cross-review.js +2 -8
  15. package/dist/arena/phases/debate-rounds.js +1 -4
  16. package/dist/arena/phases/participant-research.js +1 -4
  17. package/dist/arena/phases/planning-detail-expansion.js +8 -10
  18. package/dist/arena/planner.js +0 -1
  19. package/dist/arena/providers/docs.d.ts +1 -0
  20. package/dist/arena/providers/docs.js +5 -2
  21. package/dist/arena/providers/repo.d.ts +1 -0
  22. package/dist/arena/providers/repo.js +5 -2
  23. package/dist/arena/strategies/utils.js +36 -3
  24. package/dist/arena/transitions.d.ts +7 -2
  25. package/dist/arena/transitions.js +9 -4
  26. package/dist/arena/types.d.ts +8 -2
  27. package/dist/automation/cron-expr.d.ts +31 -0
  28. package/dist/automation/cron-expr.js +151 -0
  29. package/dist/automation/index.d.ts +41 -0
  30. package/dist/automation/index.js +39 -0
  31. package/dist/automation/runner.d.ts +67 -0
  32. package/dist/automation/runner.js +56 -0
  33. package/dist/automation/scheduler.d.ts +156 -0
  34. package/dist/automation/scheduler.js +546 -0
  35. package/dist/automation/store.d.ts +39 -0
  36. package/dist/automation/store.js +119 -0
  37. package/dist/automation/write-policy.d.ts +34 -0
  38. package/dist/automation/write-policy.js +113 -0
  39. package/dist/automation/write-run.d.ts +51 -0
  40. package/dist/automation/write-run.js +38 -0
  41. package/dist/capability-control/index.d.ts +5 -0
  42. package/dist/capability-control/index.js +3 -0
  43. package/dist/capability-control/overlay.d.ts +49 -0
  44. package/dist/capability-control/overlay.js +89 -0
  45. package/dist/capability-control/project.d.ts +65 -0
  46. package/dist/capability-control/project.js +147 -0
  47. package/dist/capability-control/service.d.ts +70 -0
  48. package/dist/capability-control/service.js +147 -0
  49. package/dist/capability-control/types.d.ts +62 -0
  50. package/dist/capability-control/types.js +17 -0
  51. package/dist/cli/agent-server-stdio.d.ts +12 -1
  52. package/dist/cli/agent-server-stdio.js +118 -18
  53. package/dist/cli/agent-server-tcp.d.ts +20 -0
  54. package/dist/cli/agent-server-tcp.js +120 -0
  55. package/dist/cli/graceful-shutdown.d.ts +27 -0
  56. package/dist/cli/graceful-shutdown.js +30 -0
  57. package/dist/context/compaction.js +23 -15
  58. package/dist/context/manager.d.ts +0 -20
  59. package/dist/context/manager.js +0 -37
  60. package/dist/cron/cron-runtime.d.ts +2 -0
  61. package/dist/cron/cron-runtime.js +2 -0
  62. package/dist/cron/cron-store.d.ts +2 -0
  63. package/dist/cron/cron-store.js +2 -0
  64. package/dist/cron/scheduler.d.ts +5 -32
  65. package/dist/cron/scheduler.js +5 -113
  66. package/dist/data/openrouter-sync.js +1 -1
  67. package/dist/engine/engine.d.ts +256 -16
  68. package/dist/engine/engine.js +856 -200
  69. package/dist/engine/goal.d.ts +42 -0
  70. package/dist/engine/goal.js +52 -0
  71. package/dist/engine/image-compression.d.ts +81 -0
  72. package/dist/engine/image-compression.js +186 -0
  73. package/dist/engine/image-policy.d.ts +152 -0
  74. package/dist/engine/image-policy.js +165 -0
  75. package/dist/engine/reactive-threshold.d.ts +13 -0
  76. package/dist/engine/reactive-threshold.js +17 -0
  77. package/dist/engine/runtime.js +7 -0
  78. package/dist/engine/session-title.d.ts +13 -0
  79. package/dist/engine/session-title.js +34 -0
  80. package/dist/engine/turn-loop.d.ts +29 -8
  81. package/dist/engine/turn-loop.js +498 -316
  82. package/dist/git/parse-log.d.ts +13 -0
  83. package/dist/git/parse-log.js +21 -0
  84. package/dist/git/utils.d.ts +8 -6
  85. package/dist/git/utils.js +57 -26
  86. package/dist/git/worktree.js +33 -18
  87. package/dist/hooks/events.d.ts +23 -1
  88. package/dist/hooks/goal-stop-hook.d.ts +30 -0
  89. package/dist/hooks/goal-stop-hook.js +102 -0
  90. package/dist/hooks/hook-output.d.ts +33 -0
  91. package/dist/hooks/hook-output.js +79 -0
  92. package/dist/hooks/registry.d.ts +7 -0
  93. package/dist/hooks/registry.js +38 -1
  94. package/dist/hooks/shell-runner.d.ts +2 -0
  95. package/dist/hooks/shell-runner.js +43 -2
  96. package/dist/index.d.ts +32 -7
  97. package/dist/index.js +30 -3
  98. package/dist/llm/capabilities/reasoning-control.d.ts +24 -0
  99. package/dist/llm/capabilities/reasoning-control.js +38 -0
  100. package/dist/llm/capabilities/rules.js +33 -3
  101. package/dist/llm/capabilities/types.d.ts +22 -2
  102. package/dist/llm/clamp-max-tokens.d.ts +11 -0
  103. package/dist/llm/clamp-max-tokens.js +17 -0
  104. package/dist/llm/client-base.d.ts +41 -4
  105. package/dist/llm/client-base.js +67 -13
  106. package/dist/llm/client-factory.d.ts +3 -3
  107. package/dist/llm/client-factory.js +2 -2
  108. package/dist/llm/model-pool.d.ts +15 -11
  109. package/dist/llm/model-pool.js +23 -20
  110. package/dist/llm/provider-catalog.d.ts +2 -2
  111. package/dist/llm/providers/anthropic.d.ts +32 -2
  112. package/dist/llm/providers/anthropic.js +147 -8
  113. package/dist/llm/providers/openai.d.ts +11 -2
  114. package/dist/llm/providers/openai.js +253 -52
  115. package/dist/llm/reasoning-setting.d.ts +51 -0
  116. package/dist/llm/reasoning-setting.js +32 -0
  117. package/dist/llm/stop-reason.d.ts +13 -0
  118. package/dist/llm/stop-reason.js +21 -0
  119. package/dist/llm/strip-vision.d.ts +32 -0
  120. package/dist/llm/strip-vision.js +94 -0
  121. package/dist/llm/types.d.ts +5 -6
  122. package/dist/logging/logger.js +18 -4
  123. package/dist/logging/sanitize-messages.d.ts +10 -0
  124. package/dist/logging/sanitize-messages.js +90 -0
  125. package/dist/lsp/client.js +28 -8
  126. package/dist/lsp/manager.js +2 -1
  127. package/dist/lsp/root-path.d.ts +9 -0
  128. package/dist/lsp/root-path.js +12 -0
  129. package/dist/onboarding.js +18 -11
  130. package/dist/plugins/gitOps.d.ts +34 -0
  131. package/dist/plugins/gitOps.js +59 -3
  132. package/dist/plugins/installer/codex/convertAgents.d.ts +7 -0
  133. package/dist/plugins/installer/codex/convertAgents.js +55 -0
  134. package/dist/plugins/installer/codex/convertMcp.d.ts +8 -0
  135. package/dist/plugins/installer/codex/convertMcp.js +45 -0
  136. package/dist/plugins/installer/codex/convertSkills.d.ts +12 -0
  137. package/dist/plugins/installer/codex/convertSkills.js +33 -0
  138. package/dist/plugins/installer/detectFormat.d.ts +2 -0
  139. package/dist/plugins/installer/detectFormat.js +6 -0
  140. package/dist/plugins/installer/install.d.ts +7 -0
  141. package/dist/plugins/installer/install.js +99 -0
  142. package/dist/plugins/installer/installFromSource.d.ts +13 -0
  143. package/dist/plugins/installer/installFromSource.js +45 -0
  144. package/dist/plugins/installer/list.d.ts +9 -0
  145. package/dist/plugins/installer/list.js +30 -0
  146. package/dist/plugins/installer/loadPluginAgents.d.ts +3 -0
  147. package/dist/plugins/installer/loadPluginAgents.js +23 -0
  148. package/dist/plugins/installer/loadPluginMcp.d.ts +7 -0
  149. package/dist/plugins/installer/loadPluginMcp.js +74 -0
  150. package/dist/plugins/installer/parseSource.d.ts +20 -0
  151. package/dist/plugins/installer/parseSource.js +74 -0
  152. package/dist/plugins/installer/paths.d.ts +5 -0
  153. package/dist/plugins/installer/paths.js +25 -0
  154. package/dist/plugins/installer/types.d.ts +49 -0
  155. package/dist/plugins/installer/types.js +27 -0
  156. package/dist/plugins/installer/uninstall.d.ts +2 -0
  157. package/dist/plugins/installer/uninstall.js +14 -0
  158. package/dist/plugins/installer/update.d.ts +14 -0
  159. package/dist/plugins/installer/update.js +65 -0
  160. package/dist/plugins/loadPluginHooks.d.ts +8 -1
  161. package/dist/plugins/loadPluginHooks.js +11 -1
  162. package/dist/plugins/marketplaceManager.d.ts +8 -1
  163. package/dist/plugins/marketplaceManager.js +38 -4
  164. package/dist/plugins/pluginCommandHook.js +34 -0
  165. package/dist/plugins/pluginInstaller.d.ts +21 -0
  166. package/dist/plugins/pluginInstaller.js +128 -29
  167. package/dist/plugins/schemas.js +40 -6
  168. package/dist/plugins/types.d.ts +4 -0
  169. package/dist/preset/index.js +16 -0
  170. package/dist/prompt/composer.d.ts +10 -0
  171. package/dist/prompt/composer.js +29 -4
  172. package/dist/prompt/sections/orchestration.md +8 -0
  173. package/dist/protocol/chat-session-manager.d.ts +8 -1
  174. package/dist/protocol/chat-session-manager.js +10 -0
  175. package/dist/protocol/chat-session.d.ts +17 -0
  176. package/dist/protocol/chat-session.js +28 -0
  177. package/dist/protocol/redact.d.ts +50 -0
  178. package/dist/protocol/redact.js +71 -0
  179. package/dist/protocol/server.d.ts +41 -2
  180. package/dist/protocol/server.js +231 -33
  181. package/dist/protocol/tcp-transport.d.ts +45 -0
  182. package/dist/protocol/tcp-transport.js +74 -0
  183. package/dist/protocol/types.d.ts +23 -0
  184. package/dist/protocol/types.js +6 -0
  185. package/dist/remote/bridge.d.ts +11 -2
  186. package/dist/remote/bridge.js +61 -33
  187. package/dist/run/ArtifactTracker.js +5 -4
  188. package/dist/run/EngineRunner.d.ts +27 -1
  189. package/dist/run/EngineRunner.js +46 -8
  190. package/dist/run/FileRunStore.js +32 -8
  191. package/dist/run/RunLock.d.ts +28 -2
  192. package/dist/run/RunLock.js +49 -7
  193. package/dist/run/RunManager.js +51 -3
  194. package/dist/run/RunQueue.d.ts +1 -0
  195. package/dist/run/RunQueue.js +8 -2
  196. package/dist/run/factory.d.ts +7 -0
  197. package/dist/run/factory.js +1 -0
  198. package/dist/run/index.d.ts +2 -2
  199. package/dist/run/index.js +1 -1
  200. package/dist/run/redirect-target.d.ts +7 -0
  201. package/dist/run/redirect-target.js +13 -0
  202. package/dist/run/types.js +1 -1
  203. package/dist/services/browser-open.d.ts +13 -0
  204. package/dist/services/browser-open.js +16 -0
  205. package/dist/services/dream-consolidation.d.ts +57 -0
  206. package/dist/services/dream-consolidation.js +151 -0
  207. package/dist/services/memory-orchestrator.js +18 -0
  208. package/dist/services/notifier.d.ts +8 -0
  209. package/dist/services/notifier.js +39 -25
  210. package/dist/services/oauth.js +5 -7
  211. package/dist/services/session-memory-sort.d.ts +8 -0
  212. package/dist/services/session-memory-sort.js +9 -0
  213. package/dist/services/session-memory.js +8 -9
  214. package/dist/session/file-history.js +10 -3
  215. package/dist/session/session-manager.d.ts +23 -1
  216. package/dist/session/session-manager.js +73 -2
  217. package/dist/settings/disk-defaults.d.ts +35 -0
  218. package/dist/settings/disk-defaults.js +24 -0
  219. package/dist/settings/manager.d.ts +45 -1
  220. package/dist/settings/manager.js +121 -17
  221. package/dist/settings/manager.test.d.ts +1 -0
  222. package/dist/settings/manager.test.js +73 -0
  223. package/dist/settings/personalization.d.ts +19 -0
  224. package/dist/settings/personalization.js +7 -0
  225. package/dist/settings/schema.d.ts +1163 -126
  226. package/dist/settings/schema.js +126 -10
  227. package/dist/skills/scanner.d.ts +13 -6
  228. package/dist/skills/scanner.js +23 -4
  229. package/dist/tool-system/builtin/add-marketplace.d.ts +12 -0
  230. package/dist/tool-system/builtin/add-marketplace.js +76 -0
  231. package/dist/tool-system/builtin/agent-registry.d.ts +15 -0
  232. package/dist/tool-system/builtin/agent-registry.js +14 -4
  233. package/dist/tool-system/builtin/agent.d.ts +55 -0
  234. package/dist/tool-system/builtin/agent.js +165 -17
  235. package/dist/tool-system/builtin/apply-patch/index.js +22 -3
  236. package/dist/tool-system/builtin/arena.js +6 -7
  237. package/dist/tool-system/builtin/complete-goal.d.ts +25 -0
  238. package/dist/tool-system/builtin/complete-goal.js +45 -0
  239. package/dist/tool-system/builtin/cron.js +53 -9
  240. package/dist/tool-system/builtin/edit.d.ts +2 -1
  241. package/dist/tool-system/builtin/edit.js +6 -1
  242. package/dist/tool-system/builtin/generate-image.d.ts +25 -0
  243. package/dist/tool-system/builtin/generate-image.js +146 -0
  244. package/dist/tool-system/builtin/glob.js +7 -0
  245. package/dist/tool-system/builtin/grep.js +6 -0
  246. package/dist/tool-system/builtin/index.d.ts +18 -1
  247. package/dist/tool-system/builtin/index.js +62 -1
  248. package/dist/tool-system/builtin/notebook-edit.d.ts +2 -1
  249. package/dist/tool-system/builtin/notebook-edit.js +8 -1
  250. package/dist/tool-system/builtin/read.d.ts +2 -1
  251. package/dist/tool-system/builtin/read.js +9 -1
  252. package/dist/tool-system/builtin/skill.js +23 -5
  253. package/dist/tool-system/builtin/sleep.js +9 -3
  254. package/dist/tool-system/builtin/task.js +4 -1
  255. package/dist/tool-system/builtin/update-automation-memory.d.ts +24 -0
  256. package/dist/tool-system/builtin/update-automation-memory.js +60 -0
  257. package/dist/tool-system/builtin/view-image.d.ts +18 -0
  258. package/dist/tool-system/builtin/view-image.js +96 -0
  259. package/dist/tool-system/builtin/web-search.d.ts +4 -1
  260. package/dist/tool-system/builtin/web-search.js +36 -3
  261. package/dist/tool-system/builtin/write.d.ts +2 -1
  262. package/dist/tool-system/builtin/write.js +7 -1
  263. package/dist/tool-system/context.d.ts +43 -0
  264. package/dist/tool-system/executor.d.ts +0 -1
  265. package/dist/tool-system/executor.js +34 -40
  266. package/dist/tool-system/investigation-guard.d.ts +4 -1
  267. package/dist/tool-system/investigation-guard.js +19 -3
  268. package/dist/tool-system/mcp-manager.d.ts +36 -12
  269. package/dist/tool-system/mcp-manager.js +173 -10
  270. package/dist/tool-system/path-policy.d.ts +94 -0
  271. package/dist/tool-system/path-policy.js +279 -0
  272. package/dist/tool-system/permission.js +43 -50
  273. package/dist/tool-system/plan-mode-allowlist.d.ts +21 -0
  274. package/dist/tool-system/plan-mode-allowlist.js +43 -0
  275. package/dist/tool-system/registry.js +12 -1
  276. package/dist/tool-system/validation.d.ts +10 -0
  277. package/dist/tool-system/validation.js +16 -3
  278. package/dist/types.d.ts +90 -15
  279. package/dist/utils/format.js +4 -2
  280. package/dist/utils/lockfile.js +9 -2
  281. package/dist/utils/theme.d.ts +6 -0
  282. package/dist/utils/theme.js +4 -2
  283. package/package.json +2 -1
@@ -7,7 +7,7 @@
7
7
  * field name, rejected sampling params, thinking knob, reasoning echo —
8
8
  * is resolved through `capabilitiesFor()`, not hardcoded.
9
9
  */
10
- import type { LLMConfig, LLMResponse } from "../../types.js";
10
+ import type { ClientDefaults, LLMConfig, LLMResponse } from "../../types.js";
11
11
  import type { CreateMessageOptions } from "../types.js";
12
12
  import { LLMClientBase } from "../client-base.js";
13
13
  interface RunStreamOpts {
@@ -19,6 +19,14 @@ interface RunStreamOpts {
19
19
  * the watchdog-side text accumulator used in tests.
20
20
  */
21
21
  onChunk?: (chunk: any) => string;
22
+ /**
23
+ * Abort signal. Checked BEFORE handing each chunk to onChunk so a cancelled
24
+ * turn stops emitting text_delta immediately — the SDK keeps yielding
25
+ * already-buffered chunks after abort() until its HTTP teardown completes, and
26
+ * without this guard those buffered deltas leak to the UI after the user hit
27
+ * Stop ("content comes back after interrupt").
28
+ */
29
+ signal?: AbortSignal;
22
30
  }
23
31
  /**
24
32
  * Consume an async iterable of stream chunks with an idle watchdog.
@@ -35,7 +43,8 @@ export declare function runStreamWithWatchdog<T = any>(stream: AsyncIterable<T>,
35
43
  export declare class OpenAIClient extends LLMClientBase {
36
44
  private _client;
37
45
  private _forceMaxCompletionTokens;
38
- constructor(config: LLMConfig);
46
+ private _dropReasoningEffort;
47
+ constructor(config: LLMConfig, defaults?: ClientDefaults);
39
48
  protected initClient(): void;
40
49
  private get client();
41
50
  /**
@@ -13,6 +13,8 @@ import { ContextLimitError, LLMError, LLMRateLimitError } from "../../exceptions
13
13
  import { logger } from "../../logging/logger.js";
14
14
  import { countTokens } from "../token-counter.js";
15
15
  import { capabilitiesFor } from "../capabilities/index.js";
16
+ import { clampMaxTokens } from "../clamp-max-tokens.js";
17
+ import { stripVisionFromHistory } from "../strip-vision.js";
16
18
  import { STREAM_WATCHDOG_CONFIG, StreamIdleTimeoutError, } from "../stream-watchdog.js";
17
19
  /**
18
20
  * Consume an async iterable of stream chunks with an idle watchdog.
@@ -32,6 +34,13 @@ export async function runStreamWithWatchdog(stream, opts = {}) {
32
34
  // Fast path: watchdog disabled AND caller did not override → no overhead.
33
35
  if (!watchdogActive) {
34
36
  for await (const chunk of stream) {
37
+ // Stop consuming the moment the turn is aborted — do NOT forward more
38
+ // chunks to onChunk (which emits text_delta to the UI). The SDK may still
39
+ // be draining buffered chunks after abort(); this prevents them leaking
40
+ // post-Stop. `break` from a for-await calls the iterator's return() for us,
41
+ // letting the SDK tear the stream down.
42
+ if (opts.signal?.aborted)
43
+ break;
35
44
  if (opts.onChunk) {
36
45
  text += opts.onChunk(chunk) ?? "";
37
46
  }
@@ -46,6 +55,11 @@ export async function runStreamWithWatchdog(stream, opts = {}) {
46
55
  const iterator = stream[Symbol.asyncIterator]();
47
56
  try {
48
57
  while (true) {
58
+ // Abort short-circuit: stop before awaiting/forwarding the next chunk so
59
+ // buffered post-abort deltas never reach onChunk. The finally below calls
60
+ // iterator.return() to tear the SDK stream down.
61
+ if (opts.signal?.aborted)
62
+ break;
49
63
  const nextPromise = iterator.next();
50
64
  // Build a timeout promise that rejects if no chunk arrives in time.
51
65
  let timeoutHandle = null;
@@ -54,14 +68,34 @@ export async function runStreamWithWatchdog(stream, opts = {}) {
54
68
  reject(new StreamIdleTimeoutError(idleTimeoutMs, opts.requestId));
55
69
  }, idleTimeoutMs);
56
70
  });
71
+ // Abort promise: resolve as soon as the signal fires so a cancel mid-chunk
72
+ // (while awaiting the next delta) breaks out immediately instead of
73
+ // waiting for the next chunk or the idle deadline.
74
+ const abortCleanups = [];
75
+ const abortPromise = new Promise((resolve) => {
76
+ const sig = opts.signal;
77
+ if (!sig)
78
+ return; // never resolves → no effect on the race
79
+ if (sig.aborted) {
80
+ resolve({ aborted: true });
81
+ return;
82
+ }
83
+ const onAbort = () => resolve({ aborted: true });
84
+ sig.addEventListener("abort", onAbort, { once: true });
85
+ abortCleanups.push(() => sig.removeEventListener("abort", onAbort));
86
+ });
57
87
  let result;
58
88
  try {
59
- result = await Promise.race([nextPromise, timeoutPromise]);
89
+ result = await Promise.race([nextPromise, timeoutPromise, abortPromise]);
60
90
  }
61
91
  finally {
62
92
  if (timeoutHandle !== null)
63
93
  clearTimeout(timeoutHandle);
94
+ for (const c of abortCleanups)
95
+ c();
64
96
  }
97
+ if ("aborted" in result)
98
+ break;
65
99
  if (result.done)
66
100
  break;
67
101
  const chunk = result.value;
@@ -88,8 +122,15 @@ export class OpenAIClient extends LLMClientBase {
88
122
  // client. Cheaper and more reliable than re-deriving from the model id when
89
123
  // a new variant ships before our regex knows about it.
90
124
  _forceMaxCompletionTokens = false;
91
- constructor(config) {
92
- super(config);
125
+ // Sticky override: some gpt-5.x variants reject `reasoning_effort` when it's
126
+ // combined with `tools` on /v1/chat/completions ("Please use /v1/responses
127
+ // instead"). Once we see that 400, drop `reasoning_effort` for the lifetime
128
+ // of the client so tool-calling turns (e.g. the dream consolidation loop)
129
+ // succeed. Omitting the field just means "model default reasoning", which is
130
+ // fine for our background/aux calls.
131
+ _dropReasoningEffort = false;
132
+ constructor(config, defaults) {
133
+ super(config, defaults);
93
134
  }
94
135
  initClient() {
95
136
  // Lazy init — client created on first use
@@ -121,10 +162,10 @@ export class OpenAIClient extends LLMClientBase {
121
162
  }
122
163
  async createMessage(options) {
123
164
  return this.withRetry(async () => {
124
- // Per-call thinking wins; otherwise fall back to provider default
125
- // (settings.providers[].thinking, threaded through LLMConfig).
126
- const thinking = options.thinking ?? this.config.thinking;
127
- const messages = this.buildMessages(options.systemPrompt, options.messages, thinking);
165
+ // Per-call reasoning wins; otherwise fall back to provider default
166
+ // (settings.providers[].reasoning, threaded through LLMConfig).
167
+ const reasoning = options.reasoning ?? this.config.reasoning;
168
+ const messages = this.buildMessages(options.systemPrompt, options.messages, reasoning);
128
169
  const tools = options.tools?.length ? this.convertTools(options.tools) : undefined;
129
170
  const span = logger.span("llm.request", {
130
171
  cat: "llm",
@@ -136,8 +177,8 @@ export class OpenAIClient extends LLMClientBase {
136
177
  });
137
178
  try {
138
179
  const response = options.stream && options.onChunk
139
- ? await this.streamMessage(options, messages, tools, thinking)
140
- : await this.nonStreamMessage(options, messages, tools, thinking);
180
+ ? await this.streamMessage(options, messages, tools, reasoning)
181
+ : await this.nonStreamMessage(options, messages, tools, reasoning);
141
182
  span.end({
142
183
  stopReason: response.stopReason,
143
184
  promptTokens: response.usage?.promptTokens,
@@ -158,49 +199,59 @@ export class OpenAIClient extends LLMClientBase {
158
199
  * Centralized so both streaming and non-streaming paths agree on the
159
200
  * exact shape.
160
201
  */
161
- buildRequestBody(options, messages, tools, thinking, stream) {
202
+ buildRequestBody(options, messages, tools, reasoning, stream) {
162
203
  const cap = this.capability;
163
- const maxTokens = options.maxTokens ?? this.maxTokens;
204
+ // Clamp to the model's known output ceiling so a stale catalog value
205
+ // (e.g. 384000 inherited after a hot model switch) can't 400 a
206
+ // smaller-cap model. No known cap → send the value as-is.
207
+ const maxTokens = clampMaxTokens(options.maxTokens ?? this.maxTokens, cap.maxOutputTokens);
164
208
  // Token-limit field — capability picks `max_tokens` vs `max_completion_tokens`.
165
209
  // Sticky fallback (set by handleApiError on a 400) overrides the rule for
166
- // ids the regex hasn't learned about yet.
210
+ // ids the regex hasn't learned about yet. When neither a requested value nor
211
+ // a known cap exists, omit the field entirely and let the endpoint apply its
212
+ // own ceiling (rather than inventing 8192 and truncating long outputs).
167
213
  const useCompletion = this._forceMaxCompletionTokens || cap.tokenLimitField === "max_completion_tokens";
168
- const tokenLimit = useCompletion
169
- ? { max_completion_tokens: maxTokens }
170
- : { max_tokens: maxTokens };
214
+ const tokenLimit = maxTokens === undefined
215
+ ? {}
216
+ : useCompletion
217
+ ? { max_completion_tokens: maxTokens }
218
+ : { max_tokens: maxTokens };
171
219
  // Sampling params — only include if the model accepts them.
172
220
  const sampling = {};
173
221
  if (!cap.rejectedParams.has("temperature")) {
174
222
  sampling.temperature =
175
223
  options.temperature !== undefined ? options.temperature : this.temperature;
176
224
  }
177
- // Reasoning shape — different vendors, different fields, never combine.
178
- // We treat `options.thinking` as the user's intent ("enabled"/"disabled")
179
- // and translate to whichever wire shape the model expects.
180
- const reasoning = {};
181
- if (thinking) {
225
+ // Reasoning shape — translate the user's ReasoningSetting to the wire
226
+ // shape. Different vendors, different fields, never combine. We read the
227
+ // real picked level (no "medium" hardcode) only fall back to "medium"
228
+ // when the setting says "thinking on" but carries no explicit effort
229
+ // ({mode:"on"}).
230
+ const reasoningBody = {};
231
+ if (reasoning && reasoning.mode !== "off") {
182
232
  switch (cap.reasoning.kind) {
183
233
  case "deepseek-thinking":
184
234
  // DeepSeek V4, Z.AI GLM-4.5+ — top-level {thinking: {type}}.
185
- reasoning.thinking = { type: thinking };
235
+ // Binary: any non-off means thinking on (effort irrelevant).
236
+ reasoningBody.thinking = { type: "enabled" };
186
237
  break;
187
238
  case "openai-effort":
188
239
  // OpenAI o-series, gpt-5+, Gemini OpenAI-compat, xAI grok-4.3,
189
240
  // Mistral magistral, Groq reasoning models — `reasoning_effort`.
190
- // "enabled" "medium" (safe middle). "disabled" → the capability's
191
- // `disabledEffort` value; defaults to "minimal" (OpenAI), but xAI
192
- // uses "low" (no minimal) and Mistral uses "none" (only high|none).
193
- reasoning.reasoning_effort =
194
- thinking === "disabled"
195
- ? (cap.reasoning.disabledEffort ?? "minimal")
196
- : "medium";
241
+ // Send the user's real level; {mode:"on"} (no level) "medium".
242
+ //
243
+ // Skip entirely once the endpoint has told us `reasoning_effort` is
244
+ // incompatible with `tools` here (see _dropReasoningEffort) — sending
245
+ // it again would just re-trigger the same 400.
246
+ if (!this._dropReasoningEffort) {
247
+ reasoningBody.reasoning_effort =
248
+ reasoning.mode === "effort" ? reasoning.effort : "medium";
249
+ }
197
250
  break;
198
251
  case "openrouter-reasoning":
199
- // OpenRouter normalized shape — {reasoning: {effort, exclude}}.
200
- reasoning.reasoning =
201
- thinking === "disabled"
202
- ? { effort: "minimal", exclude: true }
203
- : { effort: "medium" };
252
+ // OpenRouter normalized shape — {reasoning: {effort}}.
253
+ reasoningBody.reasoning =
254
+ reasoning.mode === "effort" ? { effort: reasoning.effort } : { effort: "medium" };
204
255
  break;
205
256
  case "anthropic-budget":
206
257
  case "anthropic-adaptive":
@@ -210,19 +261,39 @@ export class OpenAIClient extends LLMClientBase {
210
261
  break;
211
262
  }
212
263
  }
264
+ else if (reasoning && reasoning.mode === "off") {
265
+ // Explicit OFF — each shape's "don't think" wire form.
266
+ switch (cap.reasoning.kind) {
267
+ case "deepseek-thinking":
268
+ reasoningBody.thinking = { type: "disabled" };
269
+ break;
270
+ case "openai-effort":
271
+ // The capability's `disabledEffort` (defaults "minimal"; xAI "low",
272
+ // Mistral "none"). Skip if the endpoint already rejected the field.
273
+ if (!this._dropReasoningEffort) {
274
+ reasoningBody.reasoning_effort = cap.reasoning.disabledEffort ?? "minimal";
275
+ }
276
+ break;
277
+ case "openrouter-reasoning":
278
+ reasoningBody.reasoning = { effort: "minimal", exclude: true };
279
+ break;
280
+ default:
281
+ break;
282
+ }
283
+ }
213
284
  return {
214
285
  model: this.model,
215
286
  messages,
216
287
  ...tokenLimit,
217
288
  ...sampling,
218
- ...reasoning,
289
+ ...reasoningBody,
219
290
  ...(tools ? { tools } : {}),
220
291
  ...(stream ? { stream: true, stream_options: { include_usage: true } } : {}),
221
292
  };
222
293
  }
223
- async nonStreamMessage(options, messages, tools, thinking) {
294
+ async nonStreamMessage(options, messages, tools, reasoning) {
224
295
  try {
225
- const response = await this.client.chat.completions.create(this.buildRequestBody(options, messages, tools, thinking, false), { signal: options.signal });
296
+ const response = await this.client.chat.completions.create(this.buildRequestBody(options, messages, tools, reasoning, false), { signal: options.signal });
226
297
  const choice = response.choices[0];
227
298
  if (!choice)
228
299
  throw new LLMError("No response from OpenAI", "openai");
@@ -239,13 +310,18 @@ export class OpenAIClient extends LLMClientBase {
239
310
  throw err;
240
311
  }
241
312
  }
242
- async streamMessage(options, messages, tools, thinking) {
313
+ async streamMessage(options, messages, tools, reasoning) {
243
314
  try {
244
- const stream = await this.client.chat.completions.create(this.buildRequestBody(options, messages, tools, thinking, true), { signal: options.signal });
315
+ const stream = await this.client.chat.completions.create(this.buildRequestBody(options, messages, tools, reasoning, true), { signal: options.signal });
245
316
  let text = "";
246
317
  let reasoningContent = "";
247
318
  const toolCallsMap = new Map();
248
319
  let streamUsage;
320
+ // Last finish_reason seen on the stream. Without this we returned a
321
+ // hardcoded "stop", so an output-cap cutoff (finish_reason "length")
322
+ // was indistinguishable from a clean finish and the turn loop never
323
+ // ran its max-output continuation. Capture it and return it verbatim.
324
+ let finishReason;
249
325
  // TTFT — first chunk that actually carried text. Tool-call-only chunks
250
326
  // earlier in the stream don't count: the user-visible "text starts now"
251
327
  // moment is what we want to compare across providers.
@@ -257,6 +333,11 @@ export class OpenAIClient extends LLMClientBase {
257
333
  if (chunk.usage) {
258
334
  streamUsage = chunk.usage;
259
335
  }
336
+ // Capture finish_reason BEFORE the no-delta early return below: the
337
+ // final chunk frequently carries finish_reason with an empty delta.
338
+ const chunkFinish = chunk.choices?.[0]?.finish_reason;
339
+ if (chunkFinish)
340
+ finishReason = chunkFinish;
260
341
  const delta = chunk.choices[0]?.delta;
261
342
  if (!delta)
262
343
  return "";
@@ -329,6 +410,7 @@ export class OpenAIClient extends LLMClientBase {
329
410
  : undefined,
330
411
  requestId,
331
412
  onChunk: handleChunk,
413
+ signal: options.signal,
332
414
  });
333
415
  const toolCalls = [];
334
416
  for (const [, tc] of toolCallsMap) {
@@ -349,7 +431,7 @@ export class OpenAIClient extends LLMClientBase {
349
431
  text,
350
432
  toolCalls,
351
433
  usage,
352
- stopReason: "stop",
434
+ stopReason: finishReason ?? "stop",
353
435
  ...(reasoningContent ? { reasoningContent } : {}),
354
436
  };
355
437
  }
@@ -384,8 +466,14 @@ export class OpenAIClient extends LLMClientBase {
384
466
  ...(reasoningContent ? { reasoningContent } : {}),
385
467
  };
386
468
  }
387
- buildMessages(systemPrompt, messages, thinking) {
469
+ buildMessages(systemPrompt, messages, reasoning) {
388
470
  const result = [{ role: "system", content: systemPrompt }];
471
+ // Drop historical image blocks when the active model can't accept vision.
472
+ // Engine.run only gates *new* attachments; an image left in history from
473
+ // when a vision model was active otherwise re-serializes into `image_url`
474
+ // below and 400s ("unknown variant `image_url`") after a model switch.
475
+ // Identity-preserving on the common path (vision models / no images).
476
+ messages = stripVisionFromHistory(messages, this.capability.supportsVision);
389
477
  // Reasoning-content echo-back contract — driven by capability:
390
478
  // "when-tools" : backfill an empty placeholder if the prior assistant
391
479
  // turn doesn't carry one (DeepSeek V4 + tools 400s
@@ -398,7 +486,7 @@ export class OpenAIClient extends LLMClientBase {
398
486
  const cap = this.capability;
399
487
  const hasTools = messages.some((m) => Array.isArray(m.content) &&
400
488
  m.content.some((b) => b.type === "tool_use" || b.type === "tool_result"));
401
- const needsReasoningBackfill = thinking !== "disabled" &&
489
+ const needsReasoningBackfill = reasoning?.mode !== "off" &&
402
490
  cap.echoReasoning === "when-tools" &&
403
491
  hasTools;
404
492
  const stripReasoning = cap.echoReasoning === "never";
@@ -492,10 +580,44 @@ export class OpenAIClient extends LLMClientBase {
492
580
  const toolResults = [];
493
581
  for (const block of msg.content) {
494
582
  if (block.type === "tool_result" && block.tool_use_id) {
495
- toolResults.push({
496
- tool_use_id: block.tool_use_id,
497
- content: typeof block.content === "string" ? block.content : "",
498
- });
583
+ if (typeof block.content === "string") {
584
+ toolResults.push({
585
+ tool_use_id: block.tool_use_id,
586
+ content: block.content,
587
+ });
588
+ }
589
+ else if (Array.isArray(block.content)) {
590
+ // view_image returns an image inside tool_result.content. OpenAI's
591
+ // role:"tool" message can't carry an image, so split it: text stays
592
+ // in the tool message, image blocks are hoisted into imageParts and
593
+ // get emitted as their own user image_url message below.
594
+ const texts = [];
595
+ for (const inner of block.content) {
596
+ if (inner.type === "text" && inner.text) {
597
+ texts.push(inner.text);
598
+ }
599
+ else if (inner.type === "image" && inner.source) {
600
+ const wireDetail = mapImageDetailToOpenAI(this.imageDetail);
601
+ imageParts.push({
602
+ type: "image_url",
603
+ image_url: {
604
+ url: `data:${inner.source.media_type};base64,${inner.source.data}`,
605
+ ...(wireDetail ? { detail: wireDetail } : {}),
606
+ },
607
+ });
608
+ }
609
+ }
610
+ toolResults.push({
611
+ tool_use_id: block.tool_use_id,
612
+ content: texts.length > 0 ? texts.join("\n") : "[image returned to user message]",
613
+ });
614
+ }
615
+ else {
616
+ toolResults.push({
617
+ tool_use_id: block.tool_use_id,
618
+ content: "",
619
+ });
620
+ }
499
621
  }
500
622
  else if (block.type === "text" && block.text) {
501
623
  textParts.push(block.text);
@@ -503,13 +625,25 @@ export class OpenAIClient extends LLMClientBase {
503
625
  else if (block.type === "image" && block.source) {
504
626
  // OpenAI-compat image_url: every supported provider (OpenAI,
505
627
  // OpenRouter, OpenAI-compatible proxies for Gemini/xAI/etc)
506
- // accepts a base64 data URL as the URL. Per-(provider, model)
507
- // vision-capability gating happens earlier in Engine.run
508
- // by the time we reach here, the model supports vision.
628
+ // accepts a base64 data URL as the URL. Non-vision models never
629
+ // reach here stripVisionFromHistory() (top of buildMessages)
630
+ // has already swapped their image blocks for text placeholders,
631
+ // and Engine.run rejects *new* attachments to non-vision models.
632
+ //
633
+ // The `detail` hint is honored by OpenAI; OpenAI-compat
634
+ // proxies (OpenRouter for non-OpenAI models, etc.) tolerate
635
+ // the field even when their backend ignores it, so it's
636
+ // safe to always set when settings.images.detail is on.
637
+ // OpenAI's wire only accepts "low" / "high" / "auto"; map
638
+ // our internal "original" (a Codex-style high-fidelity
639
+ // marker) to "high" since OpenAI server-side scales 2048+
640
+ // images anyway.
641
+ const wireDetail = mapImageDetailToOpenAI(this.imageDetail);
509
642
  imageParts.push({
510
643
  type: "image_url",
511
644
  image_url: {
512
645
  url: `data:${block.source.media_type};base64,${block.source.data}`,
646
+ ...(wireDetail ? { detail: wireDetail } : {}),
513
647
  },
514
648
  });
515
649
  }
@@ -543,6 +677,17 @@ export class OpenAIClient extends LLMClientBase {
543
677
  return result;
544
678
  }
545
679
  convertTools(tools) {
680
+ // DEBUG: OpenAI rejects tool names that don't match ^[a-zA-Z0-9_-]+$.
681
+ // Dump the full list with index + source so we can pin which tool
682
+ // (e.g. tools[33]) carries the illegal name. Remove once diagnosed.
683
+ const NAME_RE = /^[a-zA-Z0-9_-]+$/;
684
+ logger.info("openai.convertTools.names", {
685
+ count: tools.length,
686
+ names: tools.map((t, i) => `[${i}] ${t.name}${NAME_RE.test(t.name) ? "" : " <-- ILLEGAL"} (src=${t.source ?? "?"})`),
687
+ illegal: tools
688
+ .map((t, i) => ({ i, name: t.name, source: t.source }))
689
+ .filter((x) => !NAME_RE.test(x.name)),
690
+ });
546
691
  return tools.map((t) => ({
547
692
  type: "function",
548
693
  function: {
@@ -553,22 +698,57 @@ export class OpenAIClient extends LLMClientBase {
553
698
  }));
554
699
  }
555
700
  handleApiError(err) {
701
+ // User pressed ESC / Stop — the SDK throws APIUserAbortError when
702
+ // the request's AbortSignal fires mid-flight. Rethrow it unchanged
703
+ // so callers up the chain (turn-loop → server.ts) can recognise it
704
+ // as a cancellation rather than a real API failure. Wrapping it
705
+ // into "OpenAI API error: Request was aborted" was surfacing a
706
+ // scary toast for what is, from the user's perspective, "I clicked
707
+ // Stop and it worked."
708
+ if (err instanceof OpenAI.APIUserAbortError) {
709
+ throw err;
710
+ }
556
711
  if (err instanceof OpenAI.APIError) {
557
712
  if (err.status === 429) {
558
713
  throw new LLMRateLimitError("openai");
559
714
  }
560
715
  const msg = (err.message ?? "").toLowerCase();
716
+ // Some 400s are deterministically self-correctable: we flip a sticky
717
+ // flag that changes the NEXT request body. For those we rethrow a
718
+ // STATUS-LESS LLMError so withRetry's isClientError() check doesn't bail
719
+ // (4xx is normally non-retryable) and the immediate retry goes out with
720
+ // the corrected body — fixing the call that triggered it, not just the
721
+ // next one.
722
+ let selfCorrected = false;
561
723
  // o-series / gpt-5+ reject `max_tokens` and demand
562
724
  // `max_completion_tokens`. The id-based regex catches the common
563
725
  // cases; this is the belt-and-suspenders path for ids that ship
564
726
  // before the regex knows about them (e.g. new `gpt-5.x` variants
565
- // routed via OpenAI-compatible proxies). Flip a sticky flag so the
566
- // next request — including `withRetry`'s next attempt — sends the
567
- // right field, instead of looping on the same 400.
727
+ // routed via OpenAI-compatible proxies).
568
728
  if (err.status === 400 &&
569
729
  msg.includes("max_tokens") &&
570
- msg.includes("max_completion_tokens")) {
730
+ msg.includes("max_completion_tokens") &&
731
+ !this._forceMaxCompletionTokens) {
571
732
  this._forceMaxCompletionTokens = true;
733
+ selfCorrected = true;
734
+ }
735
+ // gpt-5.x: "Function tools with reasoning_effort are not supported for
736
+ // <model> in /v1/chat/completions. Please use /v1/responses instead."
737
+ // Drop reasoning_effort for the lifetime of the client so the retry —
738
+ // and every later tool-calling turn — goes through. We can't switch to
739
+ // /v1/responses here, but tool calls work on /v1/chat/completions as long
740
+ // as reasoning_effort is absent.
741
+ if (err.status === 400 &&
742
+ msg.includes("reasoning_effort") &&
743
+ (msg.includes("tools") || msg.includes("/v1/responses")) &&
744
+ !this._dropReasoningEffort) {
745
+ this._dropReasoningEffort = true;
746
+ selfCorrected = true;
747
+ }
748
+ if (selfCorrected) {
749
+ // No status in details → withRetry treats it as retryable and reissues
750
+ // with the now-corrected request body.
751
+ throw new LLMError(`OpenAI API error (auto-correcting): ${err.message}`, "openai");
572
752
  }
573
753
  if (msg.includes("context_length_exceeded") ||
574
754
  msg.includes("maximum context length") ||
@@ -604,3 +784,24 @@ function extractReasoningContent(msg) {
604
784
  const candidate = msg.reasoning_content ?? msg.reasoning;
605
785
  return typeof candidate === "string" && candidate.length > 0 ? candidate : undefined;
606
786
  }
787
+ /**
788
+ * Map our internal image-detail enum to the OpenAI wire enum.
789
+ *
790
+ * OpenAI accepts only `low` / `high` / `auto`. We carry an extra
791
+ * `original` value through settings/config to match the Codex
792
+ * concept (preserve client-side dimensions, most expensive), but on
793
+ * the wire it has to collapse to `high` — OpenAI's server scales
794
+ * 2048+ images down regardless, so this is the closest faithful
795
+ * mapping.
796
+ *
797
+ * Returns undefined when the caller didn't set a detail at all, so
798
+ * the OpenAI client uses its own default ("auto", equivalent to
799
+ * "high" today).
800
+ */
801
+ function mapImageDetailToOpenAI(detail) {
802
+ if (!detail)
803
+ return undefined;
804
+ if (detail === "low")
805
+ return "low";
806
+ return "high";
807
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * ReasoningSetting — the rich, normalized reasoning/thinking config that
3
+ * replaces the old binary `thinking: "enabled"|"disabled"`.
4
+ *
5
+ * - off : no thinking (openai-effort → disabledEffort; deepseek → type:disabled; openrouter → exclude)
6
+ * - on : binary "thinking on" for deepseek-thinking / zai (no effort levels)
7
+ * - effort : openai-effort / openrouter — pick a level
8
+ * - budget : anthropic-budget — explicit thinking token budget
9
+ *
10
+ * `normalizeReasoning` accepts the legacy "enabled"/"disabled" strings so any
11
+ * lingering caller/config still works (mapped to on/off).
12
+ */
13
+ import { z } from "zod";
14
+ import type { ReasoningEffort } from "./capabilities/types.js";
15
+ export declare const REASONING_EFFORTS: readonly ["minimal", "low", "medium", "high", "xhigh"];
16
+ export declare const ReasoningSettingSchema: z.ZodDiscriminatedUnion<"mode", [z.ZodObject<{
17
+ mode: z.ZodLiteral<"off">;
18
+ }, "strip", z.ZodTypeAny, {
19
+ mode: "off";
20
+ }, {
21
+ mode: "off";
22
+ }>, z.ZodObject<{
23
+ mode: z.ZodLiteral<"on">;
24
+ }, "strip", z.ZodTypeAny, {
25
+ mode: "on";
26
+ }, {
27
+ mode: "on";
28
+ }>, z.ZodObject<{
29
+ mode: z.ZodLiteral<"effort">;
30
+ effort: z.ZodEnum<["minimal", "low", "medium", "high", "xhigh"]>;
31
+ }, "strip", z.ZodTypeAny, {
32
+ mode: "effort";
33
+ effort: "minimal" | "low" | "medium" | "high" | "xhigh";
34
+ }, {
35
+ mode: "effort";
36
+ effort: "minimal" | "low" | "medium" | "high" | "xhigh";
37
+ }>, z.ZodObject<{
38
+ mode: z.ZodLiteral<"budget">;
39
+ budgetTokens: z.ZodNumber;
40
+ }, "strip", z.ZodTypeAny, {
41
+ mode: "budget";
42
+ budgetTokens: number;
43
+ }, {
44
+ mode: "budget";
45
+ budgetTokens: number;
46
+ }>]>;
47
+ export type ReasoningSetting = z.infer<typeof ReasoningSettingSchema>;
48
+ /** Coerce legacy "enabled"/"disabled" or an object into a ReasoningSetting. */
49
+ export declare function normalizeReasoning(raw: ReasoningSetting | "enabled" | "disabled" | undefined): ReasoningSetting | undefined;
50
+ /** Effort to send when a model wants "thinking on" but the user picked no level. */
51
+ export declare const DEFAULT_EFFORT: ReasoningEffort;
@@ -0,0 +1,32 @@
1
+ /**
2
+ * ReasoningSetting — the rich, normalized reasoning/thinking config that
3
+ * replaces the old binary `thinking: "enabled"|"disabled"`.
4
+ *
5
+ * - off : no thinking (openai-effort → disabledEffort; deepseek → type:disabled; openrouter → exclude)
6
+ * - on : binary "thinking on" for deepseek-thinking / zai (no effort levels)
7
+ * - effort : openai-effort / openrouter — pick a level
8
+ * - budget : anthropic-budget — explicit thinking token budget
9
+ *
10
+ * `normalizeReasoning` accepts the legacy "enabled"/"disabled" strings so any
11
+ * lingering caller/config still works (mapped to on/off).
12
+ */
13
+ import { z } from "zod";
14
+ export const REASONING_EFFORTS = ["minimal", "low", "medium", "high", "xhigh"];
15
+ export const ReasoningSettingSchema = z.discriminatedUnion("mode", [
16
+ z.object({ mode: z.literal("off") }),
17
+ z.object({ mode: z.literal("on") }),
18
+ z.object({ mode: z.literal("effort"), effort: z.enum(REASONING_EFFORTS) }),
19
+ z.object({ mode: z.literal("budget"), budgetTokens: z.number().int().positive() }),
20
+ ]);
21
+ /** Coerce legacy "enabled"/"disabled" or an object into a ReasoningSetting. */
22
+ export function normalizeReasoning(raw) {
23
+ if (raw == null)
24
+ return undefined;
25
+ if (raw === "enabled")
26
+ return { mode: "on" };
27
+ if (raw === "disabled")
28
+ return { mode: "off" };
29
+ return raw;
30
+ }
31
+ /** Effort to send when a model wants "thinking on" but the user picked no level. */
32
+ export const DEFAULT_EFFORT = "medium";
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Shared stop-reason helpers.
3
+ *
4
+ * Providers report "the response was cut off at the output-token cap" with
5
+ * different finish/stop-reason strings:
6
+ * - OpenAI / OpenAI-compat: finish_reason "length"
7
+ * - Anthropic: stop_reason "max_tokens"
8
+ *
9
+ * The turn loop uses this to decide whether to run a max-output
10
+ * continuation, so the check must accept both spellings.
11
+ */
12
+ /** True when the stop reason indicates the model hit its output-token cap. */
13
+ export declare function isTruncatedStop(stopReason: string | undefined): boolean;
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Shared stop-reason helpers.
3
+ *
4
+ * Providers report "the response was cut off at the output-token cap" with
5
+ * different finish/stop-reason strings:
6
+ * - OpenAI / OpenAI-compat: finish_reason "length"
7
+ * - Anthropic: stop_reason "max_tokens"
8
+ *
9
+ * The turn loop uses this to decide whether to run a max-output
10
+ * continuation, so the check must accept both spellings.
11
+ */
12
+ const TRUNCATED_STOP_REASONS = new Set([
13
+ "length", // OpenAI / OpenAI-compatible
14
+ "max_tokens", // Anthropic
15
+ ]);
16
+ /** True when the stop reason indicates the model hit its output-token cap. */
17
+ export function isTruncatedStop(stopReason) {
18
+ if (!stopReason)
19
+ return false;
20
+ return TRUNCATED_STOP_REASONS.has(stopReason);
21
+ }