@cjhyy/code-shell-core 0.5.0-rc.1 → 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.
- package/dist/agent/agent-definition-registry.d.ts +18 -3
- package/dist/agent/agent-definition-registry.js +47 -18
- package/dist/agent/agent-definition.d.ts +17 -0
- package/dist/agent/agent-definition.js +22 -1
- package/dist/arena/arena.js +11 -12
- package/dist/arena/context/context-tools.js +2 -4
- package/dist/arena/context/within-root.d.ts +7 -0
- package/dist/arena/context/within-root.js +15 -0
- package/dist/arena/iterate/phases/argue.js +8 -2
- package/dist/arena/iterate/phases/revise.js +1 -1
- package/dist/arena/iterate/phases/tournament.js +3 -3
- package/dist/arena/phases/adjudication.js +1 -4
- package/dist/arena/phases/build-consensus.js +1 -4
- package/dist/arena/phases/cross-review.js +2 -8
- package/dist/arena/phases/debate-rounds.js +1 -4
- package/dist/arena/phases/participant-research.js +1 -4
- package/dist/arena/phases/planning-detail-expansion.js +8 -10
- package/dist/arena/planner.js +0 -1
- package/dist/arena/providers/docs.d.ts +1 -0
- package/dist/arena/providers/docs.js +5 -2
- package/dist/arena/providers/repo.d.ts +1 -0
- package/dist/arena/providers/repo.js +5 -2
- package/dist/arena/strategies/utils.js +36 -3
- package/dist/arena/transitions.d.ts +7 -2
- package/dist/arena/transitions.js +9 -4
- package/dist/arena/types.d.ts +8 -2
- package/dist/automation/cron-expr.d.ts +31 -0
- package/dist/automation/cron-expr.js +151 -0
- package/dist/automation/index.d.ts +41 -0
- package/dist/automation/index.js +39 -0
- package/dist/automation/runner.d.ts +67 -0
- package/dist/automation/runner.js +56 -0
- package/dist/automation/scheduler.d.ts +156 -0
- package/dist/automation/scheduler.js +546 -0
- package/dist/automation/store.d.ts +39 -0
- package/dist/automation/store.js +119 -0
- package/dist/automation/write-policy.d.ts +34 -0
- package/dist/automation/write-policy.js +113 -0
- package/dist/automation/write-run.d.ts +51 -0
- package/dist/automation/write-run.js +38 -0
- package/dist/capability-control/index.d.ts +5 -0
- package/dist/capability-control/index.js +3 -0
- package/dist/capability-control/overlay.d.ts +49 -0
- package/dist/capability-control/overlay.js +89 -0
- package/dist/capability-control/project.d.ts +65 -0
- package/dist/capability-control/project.js +147 -0
- package/dist/capability-control/service.d.ts +70 -0
- package/dist/capability-control/service.js +147 -0
- package/dist/capability-control/types.d.ts +62 -0
- package/dist/capability-control/types.js +17 -0
- package/dist/cli/agent-server-stdio.d.ts +12 -1
- package/dist/cli/agent-server-stdio.js +114 -19
- package/dist/cli/agent-server-tcp.d.ts +20 -0
- package/dist/cli/agent-server-tcp.js +120 -0
- package/dist/cli/graceful-shutdown.d.ts +27 -0
- package/dist/cli/graceful-shutdown.js +30 -0
- package/dist/context/compaction.js +23 -15
- package/dist/context/manager.d.ts +0 -20
- package/dist/context/manager.js +0 -37
- package/dist/cron/cron-runtime.d.ts +2 -0
- package/dist/cron/cron-runtime.js +2 -0
- package/dist/cron/cron-store.d.ts +2 -0
- package/dist/cron/cron-store.js +2 -0
- package/dist/cron/scheduler.d.ts +5 -32
- package/dist/cron/scheduler.js +5 -113
- package/dist/data/openrouter-sync.js +1 -1
- package/dist/engine/engine.d.ts +208 -14
- package/dist/engine/engine.js +754 -169
- package/dist/engine/goal.d.ts +42 -0
- package/dist/engine/goal.js +52 -0
- package/dist/engine/image-compression.d.ts +81 -0
- package/dist/engine/image-compression.js +186 -0
- package/dist/engine/image-policy.d.ts +152 -0
- package/dist/engine/image-policy.js +165 -0
- package/dist/engine/reactive-threshold.d.ts +13 -0
- package/dist/engine/reactive-threshold.js +17 -0
- package/dist/engine/runtime.js +7 -0
- package/dist/engine/session-title.d.ts +13 -0
- package/dist/engine/session-title.js +34 -0
- package/dist/engine/turn-loop.d.ts +29 -8
- package/dist/engine/turn-loop.js +498 -316
- package/dist/git/parse-log.d.ts +13 -0
- package/dist/git/parse-log.js +21 -0
- package/dist/git/utils.d.ts +8 -6
- package/dist/git/utils.js +57 -26
- package/dist/git/worktree.js +33 -18
- package/dist/hooks/events.d.ts +23 -1
- package/dist/hooks/goal-stop-hook.d.ts +30 -0
- package/dist/hooks/goal-stop-hook.js +102 -0
- package/dist/hooks/hook-output.d.ts +33 -0
- package/dist/hooks/hook-output.js +79 -0
- package/dist/hooks/registry.d.ts +7 -0
- package/dist/hooks/registry.js +38 -1
- package/dist/hooks/shell-runner.d.ts +2 -0
- package/dist/hooks/shell-runner.js +43 -2
- package/dist/index.d.ts +31 -6
- package/dist/index.js +30 -3
- package/dist/llm/capabilities/reasoning-control.d.ts +24 -0
- package/dist/llm/capabilities/reasoning-control.js +38 -0
- package/dist/llm/capabilities/rules.js +33 -3
- package/dist/llm/capabilities/types.d.ts +22 -2
- package/dist/llm/clamp-max-tokens.d.ts +11 -0
- package/dist/llm/clamp-max-tokens.js +17 -0
- package/dist/llm/client-base.d.ts +41 -4
- package/dist/llm/client-base.js +67 -13
- package/dist/llm/client-factory.d.ts +3 -3
- package/dist/llm/client-factory.js +2 -2
- package/dist/llm/model-pool.d.ts +15 -11
- package/dist/llm/model-pool.js +23 -20
- package/dist/llm/provider-catalog.d.ts +2 -2
- package/dist/llm/providers/anthropic.d.ts +32 -2
- package/dist/llm/providers/anthropic.js +147 -8
- package/dist/llm/providers/openai.d.ts +11 -2
- package/dist/llm/providers/openai.js +253 -52
- package/dist/llm/reasoning-setting.d.ts +51 -0
- package/dist/llm/reasoning-setting.js +32 -0
- package/dist/llm/stop-reason.d.ts +13 -0
- package/dist/llm/stop-reason.js +21 -0
- package/dist/llm/strip-vision.d.ts +32 -0
- package/dist/llm/strip-vision.js +94 -0
- package/dist/llm/types.d.ts +5 -6
- package/dist/logging/logger.js +18 -4
- package/dist/logging/sanitize-messages.d.ts +10 -0
- package/dist/logging/sanitize-messages.js +90 -0
- package/dist/lsp/client.js +28 -8
- package/dist/lsp/manager.js +2 -1
- package/dist/lsp/root-path.d.ts +9 -0
- package/dist/lsp/root-path.js +12 -0
- package/dist/onboarding.js +18 -11
- package/dist/plugins/gitOps.d.ts +34 -0
- package/dist/plugins/gitOps.js +59 -3
- package/dist/plugins/installer/codex/convertAgents.d.ts +7 -0
- package/dist/plugins/installer/codex/convertAgents.js +55 -0
- package/dist/plugins/installer/codex/convertMcp.d.ts +8 -0
- package/dist/plugins/installer/codex/convertMcp.js +45 -0
- package/dist/plugins/installer/codex/convertSkills.d.ts +12 -0
- package/dist/plugins/installer/codex/convertSkills.js +33 -0
- package/dist/plugins/installer/detectFormat.d.ts +2 -0
- package/dist/plugins/installer/detectFormat.js +6 -0
- package/dist/plugins/installer/install.d.ts +7 -0
- package/dist/plugins/installer/install.js +99 -0
- package/dist/plugins/installer/installFromSource.d.ts +13 -0
- package/dist/plugins/installer/installFromSource.js +45 -0
- package/dist/plugins/installer/list.d.ts +9 -0
- package/dist/plugins/installer/list.js +30 -0
- package/dist/plugins/installer/loadPluginAgents.d.ts +3 -0
- package/dist/plugins/installer/loadPluginAgents.js +23 -0
- package/dist/plugins/installer/loadPluginMcp.d.ts +7 -0
- package/dist/plugins/installer/loadPluginMcp.js +74 -0
- package/dist/plugins/installer/parseSource.d.ts +20 -0
- package/dist/plugins/installer/parseSource.js +74 -0
- package/dist/plugins/installer/paths.d.ts +5 -0
- package/dist/plugins/installer/paths.js +25 -0
- package/dist/plugins/installer/types.d.ts +49 -0
- package/dist/plugins/installer/types.js +27 -0
- package/dist/plugins/installer/uninstall.d.ts +2 -0
- package/dist/plugins/installer/uninstall.js +14 -0
- package/dist/plugins/installer/update.d.ts +14 -0
- package/dist/plugins/installer/update.js +65 -0
- package/dist/plugins/loadPluginHooks.d.ts +8 -1
- package/dist/plugins/loadPluginHooks.js +11 -1
- package/dist/plugins/marketplaceManager.d.ts +8 -1
- package/dist/plugins/marketplaceManager.js +38 -4
- package/dist/plugins/pluginCommandHook.js +34 -0
- package/dist/plugins/pluginInstaller.d.ts +21 -0
- package/dist/plugins/pluginInstaller.js +128 -29
- package/dist/plugins/schemas.js +40 -6
- package/dist/plugins/types.d.ts +4 -0
- package/dist/preset/index.js +16 -0
- package/dist/prompt/composer.d.ts +4 -0
- package/dist/prompt/composer.js +28 -4
- package/dist/prompt/sections/orchestration.md +8 -0
- package/dist/protocol/chat-session-manager.d.ts +8 -1
- package/dist/protocol/chat-session-manager.js +10 -0
- package/dist/protocol/chat-session.d.ts +17 -0
- package/dist/protocol/chat-session.js +28 -0
- package/dist/protocol/redact.d.ts +50 -0
- package/dist/protocol/redact.js +71 -0
- package/dist/protocol/server.d.ts +41 -2
- package/dist/protocol/server.js +231 -33
- package/dist/protocol/tcp-transport.d.ts +45 -0
- package/dist/protocol/tcp-transport.js +74 -0
- package/dist/protocol/types.d.ts +23 -0
- package/dist/protocol/types.js +6 -0
- package/dist/remote/bridge.d.ts +11 -2
- package/dist/remote/bridge.js +61 -33
- package/dist/run/ArtifactTracker.js +5 -4
- package/dist/run/EngineRunner.d.ts +27 -1
- package/dist/run/EngineRunner.js +46 -8
- package/dist/run/FileRunStore.js +32 -8
- package/dist/run/RunLock.d.ts +28 -2
- package/dist/run/RunLock.js +49 -7
- package/dist/run/RunManager.js +51 -3
- package/dist/run/RunQueue.d.ts +1 -0
- package/dist/run/RunQueue.js +8 -2
- package/dist/run/factory.d.ts +7 -0
- package/dist/run/factory.js +1 -0
- package/dist/run/index.d.ts +2 -2
- package/dist/run/index.js +1 -1
- package/dist/run/redirect-target.d.ts +7 -0
- package/dist/run/redirect-target.js +13 -0
- package/dist/run/types.js +1 -1
- package/dist/services/browser-open.d.ts +13 -0
- package/dist/services/browser-open.js +16 -0
- package/dist/services/dream-consolidation.d.ts +57 -0
- package/dist/services/dream-consolidation.js +151 -0
- package/dist/services/memory-orchestrator.js +18 -0
- package/dist/services/notifier.d.ts +8 -0
- package/dist/services/notifier.js +39 -25
- package/dist/services/oauth.js +5 -7
- package/dist/services/session-memory-sort.d.ts +8 -0
- package/dist/services/session-memory-sort.js +9 -0
- package/dist/services/session-memory.js +8 -9
- package/dist/session/file-history.js +10 -3
- package/dist/session/session-manager.d.ts +23 -1
- package/dist/session/session-manager.js +73 -2
- package/dist/settings/disk-defaults.d.ts +35 -0
- package/dist/settings/disk-defaults.js +24 -0
- package/dist/settings/manager.d.ts +33 -0
- package/dist/settings/manager.js +94 -1
- package/dist/settings/personalization.d.ts +19 -0
- package/dist/settings/personalization.js +7 -0
- package/dist/settings/schema.d.ts +1142 -126
- package/dist/settings/schema.js +119 -10
- package/dist/tool-system/builtin/add-marketplace.d.ts +12 -0
- package/dist/tool-system/builtin/add-marketplace.js +76 -0
- package/dist/tool-system/builtin/agent-registry.d.ts +11 -0
- package/dist/tool-system/builtin/agent-registry.js +8 -4
- package/dist/tool-system/builtin/agent.d.ts +29 -10
- package/dist/tool-system/builtin/agent.js +108 -52
- package/dist/tool-system/builtin/apply-patch/index.js +22 -3
- package/dist/tool-system/builtin/arena.js +6 -7
- package/dist/tool-system/builtin/complete-goal.d.ts +25 -0
- package/dist/tool-system/builtin/complete-goal.js +45 -0
- package/dist/tool-system/builtin/cron.js +53 -9
- package/dist/tool-system/builtin/edit.d.ts +2 -1
- package/dist/tool-system/builtin/edit.js +6 -1
- package/dist/tool-system/builtin/generate-image.d.ts +25 -0
- package/dist/tool-system/builtin/generate-image.js +146 -0
- package/dist/tool-system/builtin/glob.js +7 -0
- package/dist/tool-system/builtin/grep.js +6 -0
- package/dist/tool-system/builtin/index.d.ts +18 -1
- package/dist/tool-system/builtin/index.js +62 -1
- package/dist/tool-system/builtin/notebook-edit.d.ts +2 -1
- package/dist/tool-system/builtin/notebook-edit.js +8 -1
- package/dist/tool-system/builtin/read.d.ts +2 -1
- package/dist/tool-system/builtin/read.js +9 -1
- package/dist/tool-system/builtin/sleep.js +9 -3
- package/dist/tool-system/builtin/task.js +4 -1
- package/dist/tool-system/builtin/update-automation-memory.d.ts +24 -0
- package/dist/tool-system/builtin/update-automation-memory.js +60 -0
- package/dist/tool-system/builtin/view-image.d.ts +18 -0
- package/dist/tool-system/builtin/view-image.js +96 -0
- package/dist/tool-system/builtin/web-search.d.ts +4 -1
- package/dist/tool-system/builtin/web-search.js +36 -3
- package/dist/tool-system/builtin/write.d.ts +2 -1
- package/dist/tool-system/builtin/write.js +7 -1
- package/dist/tool-system/context.d.ts +12 -0
- package/dist/tool-system/executor.d.ts +0 -1
- package/dist/tool-system/executor.js +34 -40
- package/dist/tool-system/investigation-guard.d.ts +4 -1
- package/dist/tool-system/investigation-guard.js +19 -3
- package/dist/tool-system/mcp-manager.d.ts +36 -12
- package/dist/tool-system/mcp-manager.js +173 -10
- package/dist/tool-system/path-policy.d.ts +94 -0
- package/dist/tool-system/path-policy.js +279 -0
- package/dist/tool-system/permission.js +43 -50
- package/dist/tool-system/plan-mode-allowlist.d.ts +21 -0
- package/dist/tool-system/plan-mode-allowlist.js +43 -0
- package/dist/tool-system/registry.js +12 -1
- package/dist/tool-system/validation.d.ts +10 -0
- package/dist/tool-system/validation.js +16 -3
- package/dist/types.d.ts +90 -15
- package/dist/utils/format.js +4 -2
- package/dist/utils/lockfile.js +9 -2
- package/dist/utils/theme.d.ts +6 -0
- package/dist/utils/theme.js +4 -2
- 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
|
-
|
|
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
|
-
|
|
92
|
-
|
|
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
|
|
125
|
-
// (settings.providers[].
|
|
126
|
-
const
|
|
127
|
-
const messages = this.buildMessages(options.systemPrompt, options.messages,
|
|
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,
|
|
140
|
-
: await this.nonStreamMessage(options, messages, tools,
|
|
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,
|
|
202
|
+
buildRequestBody(options, messages, tools, reasoning, stream) {
|
|
162
203
|
const cap = this.capability;
|
|
163
|
-
|
|
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 =
|
|
169
|
-
? {
|
|
170
|
-
:
|
|
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 —
|
|
178
|
-
//
|
|
179
|
-
//
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
191
|
-
//
|
|
192
|
-
//
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
|
200
|
-
|
|
201
|
-
|
|
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
|
-
...
|
|
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,
|
|
294
|
+
async nonStreamMessage(options, messages, tools, reasoning) {
|
|
224
295
|
try {
|
|
225
|
-
const response = await this.client.chat.completions.create(this.buildRequestBody(options, messages, tools,
|
|
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,
|
|
313
|
+
async streamMessage(options, messages, tools, reasoning) {
|
|
243
314
|
try {
|
|
244
|
-
const stream = await this.client.chat.completions.create(this.buildRequestBody(options, messages, tools,
|
|
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,
|
|
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 =
|
|
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
|
-
|
|
496
|
-
|
|
497
|
-
|
|
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.
|
|
507
|
-
//
|
|
508
|
-
//
|
|
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).
|
|
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
|
+
}
|