@a1hvdy/cc-openclaw 0.8.0 → 0.9.1
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/scripts/bench/ab-harness.d.ts +58 -0
- package/dist/scripts/bench/ab-harness.d.ts.map +1 -0
- package/dist/scripts/bench/ab-harness.js +78 -0
- package/dist/scripts/bench/ab-harness.js.map +1 -0
- package/dist/src/channels/adapter.d.ts.map +1 -0
- package/dist/src/channels/telegram/completion-summary.d.ts.map +1 -0
- package/dist/src/channels/telegram/error-renderer.d.ts.map +1 -0
- package/dist/src/channels/telegram/event-reducer.d.ts.map +1 -0
- package/dist/src/channels/telegram/index.d.ts.map +1 -0
- package/dist/src/channels/telegram/injector.d.ts.map +1 -0
- package/dist/src/channels/telegram/live-card.d.ts.map +1 -0
- package/dist/src/channels/telegram/state-machine.d.ts.map +1 -0
- package/dist/src/channels/telegram/tool-tracker.d.ts.map +1 -0
- package/dist/src/command-router/cc-handler.d.ts.map +1 -0
- package/dist/src/command-router/index.d.ts.map +1 -0
- package/dist/src/constants.d.ts.map +1 -0
- package/dist/src/council/consensus.d.ts.map +1 -0
- package/dist/src/council/council.d.ts.map +1 -0
- package/dist/src/council/index.d.ts.map +1 -0
- package/dist/src/engines/base-oneshot-session.d.ts.map +1 -0
- package/dist/src/engines/index.d.ts.map +1 -0
- package/dist/src/engines/persistent-codex-session.d.ts.map +1 -0
- package/dist/src/engines/persistent-cursor-session.d.ts.map +1 -0
- package/dist/src/engines/persistent-custom-session.d.ts.map +1 -0
- package/dist/src/engines/persistent-gemini-session.d.ts.map +1 -0
- package/dist/src/engines/persistent-session.d.ts.map +1 -0
- package/dist/src/health/handler.d.ts.map +1 -0
- package/dist/src/health/index.d.ts.map +1 -0
- package/dist/src/index.d.ts +10 -1
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +47 -7
- package/dist/src/index.js.map +1 -1
- package/dist/src/lib/auto-recovery.d.ts.map +1 -0
- package/dist/src/lib/cache-parity.d.ts.map +1 -0
- package/dist/src/lib/circuit-breaker.d.ts.map +1 -0
- package/dist/src/lib/config-service.d.ts +106 -0
- package/dist/src/lib/config-service.js +217 -0
- package/dist/src/lib/config-service.js.map +1 -0
- package/dist/src/lib/config.d.ts +33 -14
- package/dist/src/lib/config.d.ts.map +1 -0
- package/dist/src/lib/config.js +147 -34
- package/dist/src/lib/config.js.map +1 -1
- package/dist/src/lib/debug-tap.d.ts.map +1 -0
- package/dist/src/lib/drift-detector.d.ts.map +1 -0
- package/dist/src/lib/error-formatter.d.ts.map +1 -0
- package/dist/src/lib/heartbeat-workaround.d.ts.map +1 -0
- package/dist/src/lib/index.d.ts +1 -1
- package/dist/src/lib/index.d.ts.map +1 -0
- package/dist/src/lib/index.js +4 -1
- package/dist/src/lib/index.js.map +1 -1
- package/dist/src/lib/register-guard.d.ts.map +1 -0
- package/dist/src/lib/req-shape-log.d.ts +31 -0
- package/dist/src/lib/req-shape-log.js +106 -0
- package/dist/src/lib/req-shape-log.js.map +1 -0
- package/dist/src/lib/route-flag.d.ts.map +1 -0
- package/dist/src/lib/sysprompt-strip.d.ts.map +1 -0
- package/dist/src/lib/telemetry.d.ts.map +1 -0
- package/dist/src/lib/test-mode.d.ts.map +1 -0
- package/dist/src/lib/vendor-paths.d.ts.map +1 -0
- package/dist/src/logger.d.ts.map +1 -0
- package/dist/src/mcp/bridge.d.ts.map +1 -0
- package/dist/src/mcp/index.d.ts.map +1 -0
- package/dist/src/models.d.ts.map +1 -0
- package/dist/src/openai-compat/cli-stream-parser.d.ts.map +1 -0
- package/dist/src/openai-compat/index.d.ts.map +1 -0
- package/dist/src/openai-compat/message-extractor.d.ts +79 -0
- package/dist/src/openai-compat/message-extractor.js +162 -0
- package/dist/src/openai-compat/message-extractor.js.map +1 -0
- package/dist/src/openai-compat/mode-flags.d.ts +34 -0
- package/dist/src/openai-compat/mode-flags.js +44 -0
- package/dist/src/openai-compat/mode-flags.js.map +1 -0
- package/dist/src/openai-compat/non-streaming-handler.d.ts +26 -0
- package/dist/src/openai-compat/non-streaming-handler.js +108 -0
- package/dist/src/openai-compat/non-streaming-handler.js.map +1 -0
- package/dist/src/openai-compat/openai-compat.d.ts +15 -166
- package/dist/src/openai-compat/openai-compat.d.ts.map +1 -0
- package/dist/src/openai-compat/openai-compat.js +65 -849
- package/dist/src/openai-compat/openai-compat.js.map +1 -1
- package/dist/src/openai-compat/prompts.d.ts +47 -0
- package/dist/src/openai-compat/prompts.js +119 -0
- package/dist/src/openai-compat/prompts.js.map +1 -0
- package/dist/src/openai-compat/response-formatter.d.ts +33 -0
- package/dist/src/openai-compat/response-formatter.js +74 -0
- package/dist/src/openai-compat/response-formatter.js.map +1 -0
- package/dist/src/openai-compat/session-key-resolver.d.ts +41 -0
- package/dist/src/openai-compat/session-key-resolver.js +78 -0
- package/dist/src/openai-compat/session-key-resolver.js.map +1 -0
- package/dist/src/openai-compat/skill-resolver.d.ts.map +1 -0
- package/dist/src/openai-compat/sse-translator.d.ts.map +1 -0
- package/dist/src/openai-compat/status-reporter.d.ts +30 -0
- package/dist/src/openai-compat/status-reporter.js +81 -0
- package/dist/src/openai-compat/status-reporter.js.map +1 -0
- package/dist/src/openai-compat/streaming-handler.d.ts +41 -0
- package/dist/src/openai-compat/streaming-handler.js +294 -0
- package/dist/src/openai-compat/streaming-handler.js.map +1 -0
- package/dist/src/openai-compat/tool-calls-parser.d.ts +34 -0
- package/dist/src/openai-compat/tool-calls-parser.js +93 -0
- package/dist/src/openai-compat/tool-calls-parser.js.map +1 -0
- package/dist/src/openai-compat/tool-results-serializer.d.ts +60 -0
- package/dist/src/openai-compat/tool-results-serializer.js +56 -0
- package/dist/src/openai-compat/tool-results-serializer.js.map +1 -0
- package/dist/src/proxy/anthropic-adapter.d.ts.map +1 -0
- package/dist/src/proxy/handler.d.ts.map +1 -0
- package/dist/src/proxy/index.d.ts.map +1 -0
- package/dist/src/proxy/schema-cleaner.d.ts.map +1 -0
- package/dist/src/proxy/thought-cache.d.ts.map +1 -0
- package/dist/src/session/embedded-server.d.ts.map +1 -0
- package/dist/src/session/inbox-manager.d.ts.map +1 -0
- package/dist/src/session/index.d.ts.map +1 -0
- package/dist/src/session/session-manager.d.ts.map +1 -0
- package/dist/src/session-bootstrap/cwd-patch.d.ts.map +1 -0
- package/dist/src/session-bootstrap/cwd-patch.js +20 -13
- package/dist/src/session-bootstrap/cwd-patch.js.map +1 -1
- package/dist/src/session-bootstrap/index.d.ts.map +1 -0
- package/dist/src/session-bootstrap/sysprompt-strip.d.ts.map +1 -0
- package/dist/src/session-bootstrap/think-conflict-resolver.d.ts.map +1 -0
- package/dist/src/types/index.d.ts +15 -0
- package/dist/src/types/index.js +16 -0
- package/dist/src/types/index.js.map +1 -0
- package/dist/src/types/route.d.ts +41 -0
- package/dist/src/types/route.js +12 -0
- package/dist/src/types/route.js.map +1 -0
- package/dist/src/types/runtime-config.d.ts +161 -0
- package/dist/src/types/runtime-config.js +118 -0
- package/dist/src/types/runtime-config.js.map +1 -0
- package/dist/src/types/session.d.ts +48 -0
- package/dist/src/types/session.js +20 -0
- package/dist/src/types/session.js.map +1 -0
- package/dist/src/types/sse.d.ts +38 -0
- package/dist/src/types/sse.js +12 -0
- package/dist/src/types/sse.js.map +1 -0
- package/dist/src/types/tool-bridge.d.ts +81 -0
- package/dist/src/types/tool-bridge.js +34 -0
- package/dist/src/types/tool-bridge.js.map +1 -0
- package/dist/src/types/upstream.d.ts +652 -0
- package/dist/src/types/upstream.js +145 -0
- package/dist/src/types/upstream.js.map +1 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/validation.d.ts.map +1 -0
- package/dist/tests/_helpers/subprocess-mock.d.ts +35 -0
- package/dist/tests/_helpers/subprocess-mock.d.ts.map +1 -0
- package/dist/tests/_helpers/subprocess-mock.js +136 -0
- package/dist/tests/_helpers/subprocess-mock.js.map +1 -0
- package/dist/tests/auto-recovery.test.d.ts +2 -0
- package/dist/tests/auto-recovery.test.d.ts.map +1 -0
- package/dist/tests/auto-recovery.test.js +189 -0
- package/dist/tests/auto-recovery.test.js.map +1 -0
- package/dist/tests/bench-harness.test.d.ts +2 -0
- package/dist/tests/bench-harness.test.d.ts.map +1 -0
- package/dist/tests/bench-harness.test.js +21 -0
- package/dist/tests/bench-harness.test.js.map +1 -0
- package/dist/tests/cache-parity.test.d.ts +2 -0
- package/dist/tests/cache-parity.test.d.ts.map +1 -0
- package/dist/tests/cache-parity.test.js +401 -0
- package/dist/tests/cache-parity.test.js.map +1 -0
- package/dist/tests/command-router.test.d.ts +2 -0
- package/dist/tests/command-router.test.d.ts.map +1 -0
- package/dist/tests/command-router.test.js +60 -0
- package/dist/tests/command-router.test.js.map +1 -0
- package/dist/tests/council.test.d.ts +2 -0
- package/dist/tests/council.test.d.ts.map +1 -0
- package/dist/tests/council.test.js +20 -0
- package/dist/tests/council.test.js.map +1 -0
- package/dist/tests/drift-detector.test.d.ts +2 -0
- package/dist/tests/drift-detector.test.d.ts.map +1 -0
- package/dist/tests/drift-detector.test.js +268 -0
- package/dist/tests/drift-detector.test.js.map +1 -0
- package/dist/tests/eager-bootstrap-gating.test.d.ts +9 -0
- package/dist/tests/eager-bootstrap-gating.test.d.ts.map +1 -0
- package/dist/tests/eager-bootstrap-gating.test.js +97 -0
- package/dist/tests/eager-bootstrap-gating.test.js.map +1 -0
- package/dist/tests/engines.test.d.ts +2 -0
- package/dist/tests/engines.test.d.ts.map +1 -0
- package/dist/tests/engines.test.js +8 -0
- package/dist/tests/engines.test.js.map +1 -0
- package/dist/tests/error-formatter.test.d.ts +2 -0
- package/dist/tests/error-formatter.test.d.ts.map +1 -0
- package/dist/tests/error-formatter.test.js +220 -0
- package/dist/tests/error-formatter.test.js.map +1 -0
- package/dist/tests/health.test.d.ts +2 -0
- package/dist/tests/health.test.d.ts.map +1 -0
- package/dist/tests/health.test.js +110 -0
- package/dist/tests/health.test.js.map +1 -0
- package/dist/tests/heartbeat-workaround.test.d.ts +2 -0
- package/dist/tests/heartbeat-workaround.test.d.ts.map +1 -0
- package/dist/tests/heartbeat-workaround.test.js +90 -0
- package/dist/tests/heartbeat-workaround.test.js.map +1 -0
- package/dist/tests/index.test.d.ts +2 -0
- package/dist/tests/index.test.d.ts.map +1 -0
- package/dist/tests/index.test.js +7 -0
- package/dist/tests/index.test.js.map +1 -0
- package/dist/tests/lib-sysprompt-strip.test.d.ts +2 -0
- package/dist/tests/lib-sysprompt-strip.test.d.ts.map +1 -0
- package/dist/tests/lib-sysprompt-strip.test.js +145 -0
- package/dist/tests/lib-sysprompt-strip.test.js.map +1 -0
- package/dist/tests/listener-activation.test.d.ts +2 -0
- package/dist/tests/listener-activation.test.d.ts.map +1 -0
- package/dist/tests/listener-activation.test.js +87 -0
- package/dist/tests/listener-activation.test.js.map +1 -0
- package/dist/tests/mcp-bridge.test.d.ts +2 -0
- package/dist/tests/mcp-bridge.test.d.ts.map +1 -0
- package/dist/tests/mcp-bridge.test.js +137 -0
- package/dist/tests/mcp-bridge.test.js.map +1 -0
- package/dist/tests/openai-compat.test.d.ts +2 -0
- package/dist/tests/openai-compat.test.d.ts.map +1 -0
- package/dist/tests/openai-compat.test.js +8 -0
- package/dist/tests/openai-compat.test.js.map +1 -0
- package/dist/tests/proxy-heartbeat-integration.test.d.ts +15 -0
- package/dist/tests/proxy-heartbeat-integration.test.d.ts.map +1 -0
- package/dist/tests/proxy-heartbeat-integration.test.js +122 -0
- package/dist/tests/proxy-heartbeat-integration.test.js.map +1 -0
- package/dist/tests/proxy.test.d.ts +2 -0
- package/dist/tests/proxy.test.d.ts.map +1 -0
- package/dist/tests/proxy.test.js +8 -0
- package/dist/tests/proxy.test.js.map +1 -0
- package/dist/tests/register-guard-stacking.test.d.ts +2 -0
- package/dist/tests/register-guard-stacking.test.d.ts.map +1 -0
- package/dist/tests/register-guard-stacking.test.js +61 -0
- package/dist/tests/register-guard-stacking.test.js.map +1 -0
- package/dist/tests/register-guard.test.d.ts +2 -0
- package/dist/tests/register-guard.test.d.ts.map +1 -0
- package/dist/tests/register-guard.test.js +129 -0
- package/dist/tests/register-guard.test.js.map +1 -0
- package/dist/tests/route-flag-rollback.test.d.ts +2 -0
- package/dist/tests/route-flag-rollback.test.d.ts.map +1 -0
- package/dist/tests/route-flag-rollback.test.js +70 -0
- package/dist/tests/route-flag-rollback.test.js.map +1 -0
- package/dist/tests/route-flag.test.d.ts +2 -0
- package/dist/tests/route-flag.test.d.ts.map +1 -0
- package/dist/tests/route-flag.test.js +101 -0
- package/dist/tests/route-flag.test.js.map +1 -0
- package/dist/tests/session-bootstrap.test.d.ts +2 -0
- package/dist/tests/session-bootstrap.test.d.ts.map +1 -0
- package/dist/tests/session-bootstrap.test.js +183 -0
- package/dist/tests/session-bootstrap.test.js.map +1 -0
- package/dist/tests/session.test.d.ts +2 -0
- package/dist/tests/session.test.d.ts.map +1 -0
- package/dist/tests/session.test.js +17 -0
- package/dist/tests/session.test.js.map +1 -0
- package/dist/tests/state-machine.test.d.ts +2 -0
- package/dist/tests/state-machine.test.d.ts.map +1 -0
- package/dist/tests/state-machine.test.js +133 -0
- package/dist/tests/state-machine.test.js.map +1 -0
- package/dist/tests/streaming/cli-stream-parser.test.d.ts +2 -0
- package/dist/tests/streaming/cli-stream-parser.test.d.ts.map +1 -0
- package/dist/tests/streaming/cli-stream-parser.test.js +233 -0
- package/dist/tests/streaming/cli-stream-parser.test.js.map +1 -0
- package/dist/tests/streaming/feature-flag.test.d.ts +14 -0
- package/dist/tests/streaming/feature-flag.test.d.ts.map +1 -0
- package/dist/tests/streaming/feature-flag.test.js +163 -0
- package/dist/tests/streaming/feature-flag.test.js.map +1 -0
- package/dist/tests/streaming/no-tools-prompt.test.d.ts +17 -0
- package/dist/tests/streaming/no-tools-prompt.test.d.ts.map +1 -0
- package/dist/tests/streaming/no-tools-prompt.test.js +229 -0
- package/dist/tests/streaming/no-tools-prompt.test.js.map +1 -0
- package/dist/tests/streaming/skill-plus-tools.test.d.ts +14 -0
- package/dist/tests/streaming/skill-plus-tools.test.d.ts.map +1 -0
- package/dist/tests/streaming/skill-plus-tools.test.js +234 -0
- package/dist/tests/streaming/skill-plus-tools.test.js.map +1 -0
- package/dist/tests/streaming/sse-translator.test.d.ts +2 -0
- package/dist/tests/streaming/sse-translator.test.d.ts.map +1 -0
- package/dist/tests/streaming/sse-translator.test.js +227 -0
- package/dist/tests/streaming/sse-translator.test.js.map +1 -0
- package/dist/tests/streaming/tool-result-roundtrip.test.d.ts +11 -0
- package/dist/tests/streaming/tool-result-roundtrip.test.d.ts.map +1 -0
- package/dist/tests/streaming/tool-result-roundtrip.test.js +215 -0
- package/dist/tests/streaming/tool-result-roundtrip.test.js.map +1 -0
- package/dist/tests/streaming/tool-use-translation.test.d.ts +10 -0
- package/dist/tests/streaming/tool-use-translation.test.d.ts.map +1 -0
- package/dist/tests/streaming/tool-use-translation.test.js +251 -0
- package/dist/tests/streaming/tool-use-translation.test.js.map +1 -0
- package/dist/tests/telegram-bridge.test.d.ts +2 -0
- package/dist/tests/telegram-bridge.test.d.ts.map +1 -0
- package/dist/tests/telegram-bridge.test.js +17 -0
- package/dist/tests/telegram-bridge.test.js.map +1 -0
- package/dist/tests/telegram-injector.test.d.ts +2 -0
- package/dist/tests/telegram-injector.test.d.ts.map +1 -0
- package/dist/tests/telegram-injector.test.js +74 -0
- package/dist/tests/telegram-injector.test.js.map +1 -0
- package/dist/tests/telemetry.test.d.ts +2 -0
- package/dist/tests/telemetry.test.d.ts.map +1 -0
- package/dist/tests/telemetry.test.js +405 -0
- package/dist/tests/telemetry.test.js.map +1 -0
- package/dist/tests/test-mode.test.d.ts +2 -0
- package/dist/tests/test-mode.test.d.ts.map +1 -0
- package/dist/tests/test-mode.test.js +39 -0
- package/dist/tests/test-mode.test.js.map +1 -0
- package/package.json +3 -2
|
@@ -5,484 +5,86 @@
|
|
|
5
5
|
* webchat frontends (ChatGPT-Next-Web, Open WebUI, etc.) to use the plugin
|
|
6
6
|
* as a drop-in backend. Stateful sessions maximize Anthropic prompt caching.
|
|
7
7
|
*/
|
|
8
|
-
import * as http from 'node:http';
|
|
9
8
|
import * as fs from 'node:fs';
|
|
10
9
|
import * as path from 'node:path';
|
|
11
10
|
import * as os from 'node:os';
|
|
12
|
-
import { randomUUID
|
|
11
|
+
import { randomUUID } from 'node:crypto';
|
|
13
12
|
import { resolveEngineAndModel } from '../models.js';
|
|
14
|
-
import { OPENAI_COMPAT_DEFAULT_MODEL, OPENAI_COMPAT_AUTO_COMPACT_THRESHOLD,
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
13
|
+
import { OPENAI_COMPAT_DEFAULT_MODEL, OPENAI_COMPAT_AUTO_COMPACT_THRESHOLD, } from '../constants.js';
|
|
14
|
+
import { isToolsPerMessageModeEnabled, isToolStreamMode } from './mode-flags.js';
|
|
15
|
+
import { resolveSessionKey, sessionNameFromKey } from './session-key-resolver.js';
|
|
16
|
+
import { buildSessionSystemPrompt, buildToolPromptBlock } from './prompts.js';
|
|
17
|
+
import { extractUserMessage, } from './message-extractor.js';
|
|
18
|
+
import { handleNonStreaming } from './non-streaming-handler.js';
|
|
19
|
+
import { handleStreaming } from './streaming-handler.js';
|
|
20
|
+
// Re-export for backward compat — Cluster B extracted these to dedicated
|
|
21
|
+
// modules; keep the original import surface stable for any external caller.
|
|
22
|
+
// See src/openai-compat/{mode-flags,session-key-resolver,prompts,tool-calls-parser,tool-results-serializer}.ts.
|
|
23
|
+
export { isToolsPerMessageModeEnabled, isToolStreamMode } from './mode-flags.js';
|
|
24
|
+
export { resolveSessionKey, sessionNameFromKey } from './session-key-resolver.js';
|
|
25
|
+
export { noToolsSystemPrompt, buildSessionSystemPrompt, buildToolPromptBlock } from './prompts.js';
|
|
26
|
+
export { parseToolCallsFromText } from './tool-calls-parser.js';
|
|
27
|
+
export { serializeToolResults, serializeToolResultsAsBlocks, } from './tool-results-serializer.js';
|
|
28
|
+
export { extractUserMessage, } from './message-extractor.js';
|
|
29
|
+
export { formatCompletionResponse, formatCompletionChunk } from './response-formatter.js';
|
|
30
|
+
export { reportStatus, getToolDescription } from './status-reporter.js';
|
|
31
|
+
export { handleNonStreaming } from './non-streaming-handler.js';
|
|
32
|
+
export { handleStreaming } from './streaming-handler.js';
|
|
17
33
|
import { emit as emitTrajectory } from '../lib/trajectory.js';
|
|
34
|
+
import { logReqShape } from '../lib/req-shape-log.js';
|
|
18
35
|
import { formatError, ERROR_CODES } from '../lib/error-formatter.js';
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
* plugin session. In multi-caller setups (OpenClaw routing the main agent,
|
|
27
|
-
* cron jobs, and subagents through the same gateway) that previously meant
|
|
28
|
-
* every request serialized against every other and frequently picked up the
|
|
29
|
-
* wrong session's appendSystemPrompt — also a privacy leak across callers.
|
|
30
|
-
*
|
|
31
|
-
* The model is mixed into the hash so that two callers with the same system
|
|
32
|
-
* prompt but different requested models don't collide and silently get
|
|
33
|
-
* responses from the wrong model. Originally diagnosed in PR #40 by
|
|
34
|
-
* @megayounus786.
|
|
35
|
-
*/
|
|
36
|
-
/**
|
|
37
|
-
* When set (to '1', 'true', 'yes'), the proxy preserves the pre-fix behavior:
|
|
38
|
-
* - tools injected into every user message
|
|
39
|
-
* - session key NOT fingerprinted by tools (same session across tool changes)
|
|
40
|
-
* Default (unset) is the new behavior: tools embedded in session system prompt
|
|
41
|
-
* at create time + session key fingerprinted by tools. The new behavior
|
|
42
|
-
* eliminates periodic latency spikes but does not support mutating the tool
|
|
43
|
-
* list within a single session (a new session is created when tools change).
|
|
44
|
-
*/
|
|
45
|
-
export function isToolsPerMessageModeEnabled() {
|
|
46
|
-
const v = getOpenaiCompatToolsPerMessage();
|
|
47
|
-
if (!v)
|
|
48
|
-
return false;
|
|
49
|
-
const t = v.trim().toLowerCase();
|
|
50
|
-
return t === '1' || t === 'true' || t === 'yes';
|
|
51
|
-
}
|
|
52
|
-
/**
|
|
53
|
-
* Phase 2 R5: tool-stream mode flag. When `CC_OPENCLAW_TOOL_STREAM=1` AND the
|
|
54
|
-
* caller provides `tools[]`, cc-openclaw skips the defensive "no tools"
|
|
55
|
-
* system prompt and does NOT clear `sessionConfig.tools`, allowing Claude
|
|
56
|
-
* CLI's native tool_use events to flow through the new parser+translator
|
|
57
|
-
* pipeline (Phase 4 Pillar 0.5). Default off; opt-in for the new path.
|
|
58
|
-
*/
|
|
59
|
-
export function isToolStreamMode() {
|
|
60
|
-
return process.env.CC_OPENCLAW_TOOL_STREAM === '1';
|
|
61
|
-
}
|
|
62
|
-
/**
|
|
63
|
-
* Generate the "no built-in tools" system prompt preamble.
|
|
64
|
-
* The `toolLocation` parameter controls how the model is told where to find
|
|
65
|
-
* tool definitions — 'system' means "in the <available_tools> block below"
|
|
66
|
-
* (tools baked into system prompt), 'user' means "in <available_tools> tags
|
|
67
|
-
* in the user message" (legacy per-turn injection).
|
|
68
|
-
*/
|
|
69
|
-
export function noToolsSystemPrompt(toolLocation) {
|
|
70
|
-
const allowBuiltins = process.env.CC_OPENCLAW_ALLOW_BUILTINS === '1';
|
|
71
|
-
const locationHint = toolLocation === 'system'
|
|
72
|
-
? 'in the <available_tools> block below'
|
|
73
|
-
: 'in <available_tools> tags in the user message';
|
|
74
|
-
if (allowBuiltins) {
|
|
75
|
-
// Phase 2.1 dual-source mode: Claude CLI's built-in tools (Bash, Read,
|
|
76
|
-
// Write, Edit, Glob, Grep, WebFetch, WebSearch, NotebookEdit, +any
|
|
77
|
-
// MCP-bridged tools) coexist with the proxy-defined <available_tools>.
|
|
78
|
-
// The model picks whichever fits. v0.4.9 added explicit anti-hallucination
|
|
79
|
-
// language because Savvy was still emitting "I do not have access to tools"
|
|
80
|
-
// even with this preamble active — model needs strong, repeated affirmation.
|
|
81
|
-
return ('You are an AI assistant with TWO complementary tool sources available:\n' +
|
|
82
|
-
'1. Claude Code CLI built-ins: Bash, Read, Write, Edit, Glob, Grep, WebFetch, WebSearch, NotebookEdit, plus any MCP-bridged tools (gh, npm, pm2, openclaw, docker, kubectl, etc. — invoke via Bash). Invoke these directly via your normal tool_use mechanism.\n' +
|
|
83
|
-
`2. Proxy-defined tools: ${locationHint}. Use these by emitting <tool_calls> XML as instructed there.\n` +
|
|
84
|
-
'CRITICAL — anti-hallucination: NEVER claim "I have no tools", "I cannot run commands", "I do not have access to this environment", or any equivalent phrasing. You DO have full Bash, gh, npm, pm2, openclaw, file, and web access. If unsure whether a specific tool is available, ATTEMPT it; the runtime will return a concrete error if not.\n' +
|
|
85
|
-
'When a request needs action, ACT via tools — do not narrate what you would do. Default to executing, not describing.\n' +
|
|
86
|
-
'For research / web fetching / file ops / command execution: prefer the built-ins (WebFetch, WebSearch, Bash, Read).\n' +
|
|
87
|
-
'For tasks where the proxy ships a specific custom tool: prefer the proxy tool.\n' +
|
|
88
|
-
'If a tool you would normally use is not available in either source, attempt the task with what IS available and explain concretely which tool would have helped.\n' +
|
|
89
|
-
'CRITICAL — visible close-out: After completing any tool sequence, ALWAYS produce at least one short sentence of visible text summarizing what you did, what you found, or what the user should know next. NEVER end a turn on a pure tool_use → stop boundary with no text — the runtime will reject the turn as an "incomplete terminal response" and the user will see an error. Even a one-line acknowledgement like "Done — $X done; next step is Y." is sufficient. The summary text is the *only* part of the response the user reads as your reply.');
|
|
90
|
-
}
|
|
91
|
-
return ('You are an AI assistant operating through a proxy that provides a specific set of tools.\n' +
|
|
92
|
-
`Your tools are defined ${locationHint}. Use them by emitting <tool_calls> XML as instructed there.\n` +
|
|
93
|
-
'When a request needs action, you MUST use the tools that are defined — do not refuse on the grounds of "no tools".\n' +
|
|
94
|
-
'If a specific tool you would normally use is not in <available_tools>, do the best you can with what IS provided, or report concretely which tool would be needed.\n' +
|
|
95
|
-
'You do NOT have direct access to Claude Code CLI built-ins (Bash, Read, Write, Edit, Glob, Grep) outside of <available_tools>; do not invoke them directly.\n' +
|
|
96
|
-
'If no <available_tools> are provided at all, respond with text only.');
|
|
97
|
-
}
|
|
98
|
-
/**
|
|
99
|
-
* Build the full session system prompt for a Claude Code session with tools.
|
|
100
|
-
* Exported for testability — called from `handleChatCompletion`.
|
|
101
|
-
*
|
|
102
|
-
* - Default mode: tools are embedded in the system prompt (cacheable by Anthropic).
|
|
103
|
-
* - Legacy mode (OPENAI_COMPAT_TOOLS_PER_MESSAGE=1): tools are NOT embedded;
|
|
104
|
-
* they'll be injected per-turn in the user message instead.
|
|
105
|
-
*/
|
|
106
|
-
export function buildSessionSystemPrompt(tools, callerSystemPrompt) {
|
|
107
|
-
// Phase 2 R5: in tool-stream mode with tools provided, skip the defensive
|
|
108
|
-
// "no tools" preamble and the <available_tools> block entirely. Claude CLI
|
|
109
|
-
// gets the tools natively via sessionConfig.tools (not cleared) and emits
|
|
110
|
-
// tool_use events that the new parser+translator translate to OpenAI SSE.
|
|
111
|
-
// v0.4.9: prepend a minimal tool-affirmation preamble. Without this, callers
|
|
112
|
-
// with weak/no system prompts saw the model hallucinate "I have no tools" —
|
|
113
|
-
// the CLI had tools loaded but nothing in the prompt told the model so.
|
|
114
|
-
if (isToolStreamMode() && tools && tools.length > 0) {
|
|
115
|
-
const allowBuiltins = process.env.CC_OPENCLAW_ALLOW_BUILTINS === '1';
|
|
116
|
-
const toolAffirmation = allowBuiltins
|
|
117
|
-
? 'You have full Claude Code CLI tools (Bash, Read, Write, Edit, Glob, Grep, WebFetch, WebSearch, gh, npm, pm2, openclaw, etc.) available natively, plus any caller-provided tools below. NEVER claim "I have no tools" — invoke a tool and let the runtime confirm. Default to ACTING, not narrating.\n\n'
|
|
118
|
-
: '';
|
|
119
|
-
return toolAffirmation + (callerSystemPrompt ?? '');
|
|
120
|
-
}
|
|
121
|
-
if (isToolsPerMessageModeEnabled()) {
|
|
122
|
-
const preamble = noToolsSystemPrompt('user');
|
|
123
|
-
return callerSystemPrompt ? `${preamble}\n\n${callerSystemPrompt}` : preamble;
|
|
124
|
-
}
|
|
125
|
-
const preamble = noToolsSystemPrompt('system');
|
|
126
|
-
const toolBlock = buildToolPromptBlock(tools);
|
|
127
|
-
const systemWithTools = `${preamble}\n\n${toolBlock}`;
|
|
128
|
-
return callerSystemPrompt ? `${systemWithTools}\n\n${callerSystemPrompt}` : systemWithTools;
|
|
129
|
-
}
|
|
130
|
-
export function resolveSessionKey(body, headers) {
|
|
131
|
-
const headerKey = headers['x-session-id'];
|
|
132
|
-
if (typeof headerKey === 'string' && headerKey.trim())
|
|
133
|
-
return headerKey.trim();
|
|
134
|
-
if (body.user && body.user.trim())
|
|
135
|
-
return body.user.trim();
|
|
136
|
-
const sys = (body.messages || [])
|
|
137
|
-
.filter((m) => m && m.role === 'system')
|
|
138
|
-
.map((m) => (typeof m.content === 'string' ? m.content : JSON.stringify(m.content)))
|
|
139
|
-
.join('\n');
|
|
140
|
-
const modelTag = (body.model || '').toString();
|
|
141
|
-
// Include a fingerprint of the tool list so that two requests with the same
|
|
142
|
-
// system prompt but different tool definitions land in different sessions.
|
|
143
|
-
// The tool schemas are baked into the session system prompt on create; if
|
|
144
|
-
// tools change we need a new session rather than re-using a stale one.
|
|
145
|
-
// Hash only tool names + a short description prefix to keep the fingerprint
|
|
146
|
-
// small and stable against schema formatting differences.
|
|
147
|
-
//
|
|
148
|
-
// Opt-out: OPENAI_COMPAT_TOOLS_PER_MESSAGE=1 restores the pre-fix behavior
|
|
149
|
-
// of keying sessions only by system prompt + model. Enable this if you have
|
|
150
|
-
// callers that mutate their tool list within one conversation and rely on
|
|
151
|
-
// continuing history across tool changes.
|
|
152
|
-
const toolsFingerprint = isToolsPerMessageModeEnabled()
|
|
153
|
-
? ''
|
|
154
|
-
: (body.tools || [])
|
|
155
|
-
.map((t) => {
|
|
156
|
-
const fn = t?.function;
|
|
157
|
-
if (!fn?.name)
|
|
158
|
-
return '';
|
|
159
|
-
const descPrefix = (typeof fn.description === 'string' ? fn.description : '').slice(0, 64);
|
|
160
|
-
return `${fn.name}:${descPrefix}`;
|
|
161
|
-
})
|
|
162
|
-
.filter(Boolean)
|
|
163
|
-
.join('|');
|
|
164
|
-
if (sys || modelTag || toolsFingerprint) {
|
|
165
|
-
return ('sys-' +
|
|
166
|
-
createHash('sha1')
|
|
167
|
-
.update(modelTag + '\n' + sys + '\n' + toolsFingerprint)
|
|
168
|
-
.digest('hex')
|
|
169
|
-
.slice(0, 12));
|
|
170
|
-
}
|
|
171
|
-
return 'default';
|
|
172
|
-
}
|
|
173
|
-
/** Build the full session name from a key */
|
|
174
|
-
export function sessionNameFromKey(key) {
|
|
175
|
-
return `${OPENAI_COMPAT_SESSION_PREFIX}${key}`;
|
|
176
|
-
}
|
|
177
|
-
// ─── Function Calling Support ────────────────────────────────────────────────
|
|
178
|
-
/**
|
|
179
|
-
* Convert OpenAI tool definitions into a structured prompt block.
|
|
180
|
-
* Injected into the user message so the CLI model sees tool definitions
|
|
181
|
-
* and responds with <tool_calls> tags when it wants to invoke a function.
|
|
182
|
-
*/
|
|
183
|
-
export function buildToolPromptBlock(tools) {
|
|
184
|
-
if (!tools?.length)
|
|
185
|
-
return '';
|
|
186
|
-
const toolDefs = tools
|
|
187
|
-
.map((t) => {
|
|
188
|
-
const fn = t.function;
|
|
189
|
-
const params = JSON.stringify(fn.parameters, null, 2);
|
|
190
|
-
return `### ${fn.name}\n${fn.description}\n\nParameters:\n\`\`\`json\n${params}\n\`\`\``;
|
|
191
|
-
})
|
|
192
|
-
.join('\n\n');
|
|
193
|
-
return ('<available_tools>\n' +
|
|
194
|
-
'You have access to the following tools. When you need to use a tool, respond with a JSON array wrapped in <tool_calls> tags.\n\n' +
|
|
195
|
-
'FORMAT:\n' +
|
|
196
|
-
'<tool_calls>\n' +
|
|
197
|
-
'[{"name": "tool_name", "arguments": {"param1": "value1"}}]\n' +
|
|
198
|
-
'</tool_calls>\n\n' +
|
|
199
|
-
'If you do NOT need any tools, respond normally with text only (no <tool_calls> tags).\n\n' +
|
|
200
|
-
'## Available Tools\n\n' +
|
|
201
|
-
toolDefs +
|
|
202
|
-
'\n</available_tools>');
|
|
203
|
-
}
|
|
204
|
-
/**
|
|
205
|
-
* Parse tool_calls from CLI text output.
|
|
206
|
-
*
|
|
207
|
-
* Looks for <tool_calls>[...]</tool_calls> tags in the response text.
|
|
208
|
-
* Returns both the extracted text content (before/after tags) and any tool calls found.
|
|
209
|
-
*/
|
|
210
|
-
export function parseToolCallsFromText(text) {
|
|
211
|
-
// Match ALL <tool_calls> blocks (model may output multiple)
|
|
212
|
-
const tagRegex = /<tool_calls>\s*([\s\S]*?)\s*<\/tool_calls>/g;
|
|
213
|
-
const allCalls = [];
|
|
214
|
-
let lastIndex = 0;
|
|
215
|
-
const textParts = [];
|
|
216
|
-
let m;
|
|
217
|
-
while ((m = tagRegex.exec(text)) !== null) {
|
|
218
|
-
// Collect text before this block
|
|
219
|
-
const before = text.slice(lastIndex, m.index).trim();
|
|
220
|
-
if (before)
|
|
221
|
-
textParts.push(before);
|
|
222
|
-
lastIndex = m.index + m[0].length;
|
|
223
|
-
try {
|
|
224
|
-
const parsed = JSON.parse(m[1].trim());
|
|
225
|
-
const arr = Array.isArray(parsed) ? parsed : [parsed];
|
|
226
|
-
for (const raw of arr) {
|
|
227
|
-
const call = raw;
|
|
228
|
-
if (!call || typeof call !== 'object' || typeof call.name !== 'string')
|
|
229
|
-
continue;
|
|
230
|
-
let args;
|
|
231
|
-
if (typeof call.arguments === 'string') {
|
|
232
|
-
try {
|
|
233
|
-
JSON.parse(call.arguments);
|
|
234
|
-
args = call.arguments;
|
|
235
|
-
}
|
|
236
|
-
catch {
|
|
237
|
-
args = JSON.stringify({ input: call.arguments });
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
else {
|
|
241
|
-
args = JSON.stringify(call.arguments ?? {});
|
|
242
|
-
}
|
|
243
|
-
allCalls.push({
|
|
244
|
-
id: `call_${randomUUID().replace(/-/g, '').slice(0, 24)}`,
|
|
245
|
-
type: 'function',
|
|
246
|
-
function: { name: call.name, arguments: args },
|
|
247
|
-
});
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
catch {
|
|
251
|
-
// One block failed — keep its text as content
|
|
252
|
-
textParts.push(m[0]);
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
// Collect text after last block
|
|
256
|
-
const after = text.slice(lastIndex).trim();
|
|
257
|
-
if (after)
|
|
258
|
-
textParts.push(after);
|
|
259
|
-
// Strip <tool_result> and <tool_results> tags that the model may echo back
|
|
260
|
-
// from the serialized tool results we injected earlier.
|
|
261
|
-
const stripToolResultTags = (s) => s
|
|
262
|
-
.replace(/<tool_results?>[\s\S]*?<\/tool_results?>/g, '')
|
|
263
|
-
.replace(/<tool_results?[^>]*>/g, '')
|
|
264
|
-
.trim();
|
|
265
|
-
if (allCalls.length > 0) {
|
|
266
|
-
const raw = textParts.join('\n').trim();
|
|
267
|
-
const cleaned = raw ? stripToolResultTags(raw) : null;
|
|
268
|
-
return { textContent: cleaned || null, toolCalls: allCalls };
|
|
269
|
-
}
|
|
270
|
-
const cleaned = text ? stripToolResultTags(text) : null;
|
|
271
|
-
return { textContent: cleaned || null, toolCalls: [] };
|
|
272
|
-
}
|
|
273
|
-
/**
|
|
274
|
-
* Serialize tool result messages into a text block for the CLI model.
|
|
275
|
-
* Converts OpenAI `tool` role messages into <tool_result> tags.
|
|
276
|
-
*
|
|
277
|
-
* Legacy path (CC_OPENCLAW_TOOL_STREAM=0). Used when the model receives
|
|
278
|
-
* tool definitions via the system prompt's <available_tools> XML block
|
|
279
|
-
* and emits <tool_calls> XML in response. Tool-stream mode (R4) uses
|
|
280
|
-
* `serializeToolResultsAsBlocks()` instead, returning native Anthropic
|
|
281
|
-
* `tool_result` content blocks that Claude CLI parses directly.
|
|
282
|
-
*/
|
|
283
|
-
export function serializeToolResults(messages) {
|
|
284
|
-
const toolMessages = messages.filter((m) => m.role === 'tool');
|
|
285
|
-
if (!toolMessages.length)
|
|
286
|
-
return '';
|
|
287
|
-
const results = toolMessages
|
|
288
|
-
.map((m) => {
|
|
289
|
-
const content = typeof m.content === 'string' ? m.content : JSON.stringify(m.content);
|
|
290
|
-
return `<tool_result tool_call_id="${m.tool_call_id || 'unknown'}">\n${content}\n</tool_result>`;
|
|
291
|
-
})
|
|
292
|
-
.join('\n\n');
|
|
293
|
-
return `<tool_results>\n${results}\n</tool_results>\n\nAbove are the results of the tool calls you requested. Continue your response based on these results.`;
|
|
294
|
-
}
|
|
295
|
-
export function serializeToolResultsAsBlocks(messages) {
|
|
296
|
-
return messages
|
|
297
|
-
.filter((m) => m.role === 'tool')
|
|
298
|
-
.map((m) => ({
|
|
299
|
-
type: 'tool_result',
|
|
300
|
-
tool_use_id: m.tool_call_id || 'unknown',
|
|
301
|
-
content: typeof m.content === 'string' ? m.content : JSON.stringify(m.content),
|
|
302
|
-
}));
|
|
303
|
-
}
|
|
304
|
-
/**
|
|
305
|
-
* Extract the relevant parts from an OpenAI messages array.
|
|
306
|
-
*
|
|
307
|
-
* Sessions are stateful — we only need the last user message. The tricky
|
|
308
|
-
* question is whether to start a fresh session or append to the existing one.
|
|
309
|
-
*
|
|
310
|
-
* Default mode (no env var): only honor an explicit `X-Session-Reset: 1`
|
|
311
|
-
* header. This is correct for clients that maintain their own conversation
|
|
312
|
-
* transcript and forward only the latest user turn (OpenClaw main agent
|
|
313
|
-
* loop, cron jobs, subagents). The previous heuristic
|
|
314
|
-
* (`nonSystemMessages.length <= 1`) fired on every such request, killing the
|
|
315
|
-
* persistent CLI every turn and preventing Anthropic prompt caching from
|
|
316
|
-
* ever warming. Originally diagnosed in PR #40 by @megayounus786.
|
|
317
|
-
*
|
|
318
|
-
* Legacy mode (`OPENAI_COMPAT_NEW_CONVO_HEURISTIC=1`): restore the old
|
|
319
|
-
* `system + single user ⇒ new conversation` rule, for clients that re-send
|
|
320
|
-
* the full transcript on every turn (ChatGPT-Next-Web, Open WebUI, data
|
|
321
|
-
* labeling tools, etc). They use the transcript shape itself as their only
|
|
322
|
-
* "start a new conversation" signal.
|
|
323
|
-
*
|
|
324
|
-
* The env var is read on every call so ops can flip it via launchctl setenv
|
|
325
|
-
* without restarting the server.
|
|
326
|
-
*/
|
|
327
|
-
export function extractUserMessage(messages, headers) {
|
|
328
|
-
if (!messages || messages.length === 0) {
|
|
329
|
-
throw new Error('messages array is empty');
|
|
330
|
-
}
|
|
331
|
-
// Normalize content from any message: OpenAI API allows content as a string
|
|
332
|
-
// OR an array of content parts (e.g. multimodal messages with text + images).
|
|
333
|
-
// We need a string for the CLI, so arrays are joined.
|
|
334
|
-
const textOf = (m) => {
|
|
335
|
-
if (typeof m.content === 'string')
|
|
336
|
-
return m.content;
|
|
337
|
-
if (Array.isArray(m.content)) {
|
|
338
|
-
return m.content
|
|
339
|
-
.map((p) => p.text || '')
|
|
340
|
-
.filter(Boolean)
|
|
341
|
-
.join('');
|
|
342
|
-
}
|
|
343
|
-
return m.content != null ? String(m.content) : '';
|
|
344
|
-
};
|
|
345
|
-
// Extract system prompt if present
|
|
346
|
-
const systemMessages = messages.filter((m) => m.role === 'system');
|
|
347
|
-
const systemPrompt = systemMessages.length > 0 ? systemMessages.map(textOf).join('\n') : undefined;
|
|
348
|
-
// Handle tool result messages — only when the LAST non-system message is
|
|
349
|
-
// a tool role (meaning we're in an active tool-use cycle). If the last
|
|
350
|
-
// message is a user role, it's a follow-up in an existing conversation
|
|
351
|
-
// and the old tool results are already in the CLI's history.
|
|
352
|
-
const lastNonSystem = [...messages].reverse().find((m) => m.role !== 'system');
|
|
353
|
-
if (lastNonSystem?.role === 'tool') {
|
|
354
|
-
const userMessages = messages.filter((m) => m.role === 'user');
|
|
355
|
-
const lastUserText = userMessages.length > 0 ? textOf(userMessages[userMessages.length - 1]) : '';
|
|
356
|
-
// Phase 2 R4 wire-up: in tool-stream mode, emit native Anthropic
|
|
357
|
-
// tool_result blocks instead of XML-wrapped text. Claude CLI's
|
|
358
|
-
// stream-json input accepts content arrays directly.
|
|
359
|
-
if (isToolStreamMode()) {
|
|
360
|
-
const toolBlocks = serializeToolResultsAsBlocks(messages);
|
|
361
|
-
const userMessageBlocks = [...toolBlocks];
|
|
362
|
-
if (lastUserText) {
|
|
363
|
-
userMessageBlocks.push({ type: 'text', text: lastUserText });
|
|
364
|
-
}
|
|
365
|
-
// Keep userMessage populated as the legacy XML form for callers
|
|
366
|
-
// that don't yet handle the structured path. Both fields agree in
|
|
367
|
-
// intent; consumers should prefer userMessageBlocks when present.
|
|
368
|
-
const fallback = serializeToolResults(messages);
|
|
369
|
-
const userMessage = lastUserText ? `${fallback}\n\n${lastUserText}` : fallback;
|
|
370
|
-
return { systemPrompt, userMessage, userMessageBlocks, isNewConversation: false };
|
|
371
|
-
}
|
|
372
|
-
const toolResultBlock = serializeToolResults(messages);
|
|
373
|
-
const userMessage = lastUserText ? `${toolResultBlock}\n\n${lastUserText}` : toolResultBlock;
|
|
374
|
-
return { systemPrompt, userMessage, isNewConversation: false };
|
|
375
|
-
}
|
|
376
|
-
// Find last user message
|
|
377
|
-
const userMessages = messages.filter((m) => m.role === 'user');
|
|
378
|
-
if (userMessages.length === 0) {
|
|
379
|
-
throw new Error('No user message found in messages array');
|
|
380
|
-
}
|
|
381
|
-
const rawUserMessage = textOf(userMessages[userMessages.length - 1]);
|
|
382
|
-
// Workspace skill auto-inline: if the last user message is /<skill> [args]
|
|
383
|
-
// and ~/.openclaw/workspace/skills/*/SKILL.md has a matching `name:` in
|
|
384
|
-
// frontmatter, replace the user message with the SKILL.md body so the
|
|
385
|
-
// model has full skill context without needing the Read tool (cc-openclaw
|
|
386
|
-
// disables built-in tools by design — see the `sessionConfig.tools = ''`
|
|
387
|
-
// line below).
|
|
388
|
-
const userMessage = maybeInlineSkill(rawUserMessage) ?? rawUserMessage;
|
|
389
|
-
// 1. Explicit reset header — honored in both modes. Normalize trim+lowercase
|
|
390
|
-
// so callers using `TRUE`, ` 1 `, etc. don't silently fail.
|
|
391
|
-
const rawReset = headers?.['x-session-reset'];
|
|
392
|
-
const resetHeader = typeof rawReset === 'string' ? rawReset.trim().toLowerCase() : '';
|
|
393
|
-
if (resetHeader === 'true' || resetHeader === '1') {
|
|
394
|
-
return { systemPrompt, userMessage, isNewConversation: true };
|
|
36
|
+
function parseRouteBody(body) {
|
|
37
|
+
if (!body.messages || !Array.isArray(body.messages) || body.messages.length === 0) {
|
|
38
|
+
return {
|
|
39
|
+
ok: false,
|
|
40
|
+
status: 400,
|
|
41
|
+
error: 'messages is required and must be a non-empty array',
|
|
42
|
+
};
|
|
395
43
|
}
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
44
|
+
if (body.max_tokens !== undefined &&
|
|
45
|
+
(typeof body.max_tokens !== 'number' || body.max_tokens <= 0)) {
|
|
46
|
+
return {
|
|
47
|
+
ok: false,
|
|
48
|
+
status: 400,
|
|
49
|
+
error: 'max_tokens must be a positive number',
|
|
50
|
+
};
|
|
400
51
|
}
|
|
401
|
-
return { systemPrompt, userMessage, isNewConversation: false };
|
|
402
|
-
}
|
|
403
|
-
// ─── Response Formatting ─────────────────────────────────────────────────────
|
|
404
|
-
export function formatCompletionResponse(id, model, text, tokensIn, tokensOut, toolCalls,
|
|
405
|
-
/** v0.7.0: when present + non-empty, attached as `choices[0].message.reasoning`
|
|
406
|
-
* (mirrors OpenAI o1/o3 schema). Caller must already be gated on
|
|
407
|
-
* `getSurfaceThinkingEnabled()` from `lib/config.ts` — this function does
|
|
408
|
-
* not re-check the flag. Pass empty string or undefined to omit. */
|
|
409
|
-
reasoning) {
|
|
410
|
-
const hasToolCalls = toolCalls && toolCalls.length > 0;
|
|
411
|
-
const hasReasoning = typeof reasoning === 'string' && reasoning.length > 0;
|
|
412
|
-
// v0.7.2 backstop: openclaw upstream's "incomplete terminal response" classifier
|
|
413
|
-
// rejects turns with payloads=0 (no visible text) and treats them as a model error,
|
|
414
|
-
// burning a retry attempt before falling back to a different model. opus-4-7 with
|
|
415
|
-
// ALLOW_BUILTINS sometimes drifts into a tool_use → stop boundary with no text
|
|
416
|
-
// close-out. The system-prompt directive (above) is the primary fix; this is a
|
|
417
|
-
// belt-and-suspenders backstop. When `text` is empty AND we have no caller-visible
|
|
418
|
-
// tool_calls (i.e. CLI built-ins fired but legacy XML extraction yielded nothing),
|
|
419
|
-
// emit a minimal acknowledgement so the upstream classifier sees a payload.
|
|
420
|
-
// Tool_calls turns are exempt — those are openai-spec-correct with `content: null`.
|
|
421
|
-
const safeContent = text || (hasToolCalls ? null : 'Done.');
|
|
422
52
|
return {
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
...(hasToolCalls ? { tool_calls: toolCalls } : {}),
|
|
434
|
-
...(hasReasoning ? { reasoning } : {}),
|
|
435
|
-
},
|
|
436
|
-
finish_reason: hasToolCalls ? 'tool_calls' : 'stop',
|
|
437
|
-
},
|
|
438
|
-
],
|
|
439
|
-
usage: {
|
|
440
|
-
prompt_tokens: tokensIn,
|
|
441
|
-
completion_tokens: tokensOut,
|
|
442
|
-
total_tokens: tokensIn + tokensOut,
|
|
53
|
+
ok: true,
|
|
54
|
+
request: {
|
|
55
|
+
messages: body.messages,
|
|
56
|
+
model: body.model,
|
|
57
|
+
stream: body.stream,
|
|
58
|
+
temperature: body.temperature,
|
|
59
|
+
max_tokens: body.max_tokens,
|
|
60
|
+
max_completion_tokens: body.max_completion_tokens,
|
|
61
|
+
user: body.user,
|
|
62
|
+
tools: body.tools,
|
|
443
63
|
},
|
|
444
64
|
};
|
|
445
65
|
}
|
|
446
|
-
export function formatCompletionChunk(id, model, delta, finishReason) {
|
|
447
|
-
return {
|
|
448
|
-
id,
|
|
449
|
-
object: 'chat.completion.chunk',
|
|
450
|
-
created: Math.floor(Date.now() / 1000),
|
|
451
|
-
model,
|
|
452
|
-
choices: [{ index: 0, delta, finish_reason: finishReason }],
|
|
453
|
-
};
|
|
454
|
-
}
|
|
455
66
|
export async function handleChatCompletion(manager, body, headers, res) {
|
|
456
|
-
//
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
})
|
|
462
|
-
return;
|
|
463
|
-
}
|
|
464
|
-
// Safe cast: messages validated above, other fields are optional
|
|
465
|
-
const request = {
|
|
466
|
-
messages: body.messages,
|
|
467
|
-
model: body.model,
|
|
468
|
-
stream: body.stream,
|
|
469
|
-
temperature: body.temperature,
|
|
470
|
-
max_tokens: body.max_tokens,
|
|
471
|
-
user: body.user,
|
|
472
|
-
tools: body.tools,
|
|
473
|
-
};
|
|
474
|
-
// Validate max_tokens if provided
|
|
475
|
-
if (request.max_tokens !== undefined && (typeof request.max_tokens !== 'number' || request.max_tokens <= 0)) {
|
|
476
|
-
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
67
|
+
// Cluster A step 4: typed boundary parser. Replaces the inline cast +
|
|
68
|
+
// validation block that previously lived here (~30 lines). Returns a
|
|
69
|
+
// discriminated union so the type system enforces "validate before use."
|
|
70
|
+
const parsed = parseRouteBody(body);
|
|
71
|
+
if (!parsed.ok) {
|
|
72
|
+
res.writeHead(parsed.status, { 'Content-Type': 'application/json' });
|
|
477
73
|
res.end(JSON.stringify({
|
|
478
|
-
error: { message:
|
|
74
|
+
error: { message: parsed.error, type: 'invalid_request_error' },
|
|
479
75
|
}));
|
|
480
76
|
return;
|
|
481
77
|
}
|
|
78
|
+
const request = parsed.request;
|
|
482
79
|
const modelStr = request.model || OPENAI_COMPAT_DEFAULT_MODEL;
|
|
483
80
|
const { engine, model: resolvedModel } = resolveEngineAndModel(modelStr);
|
|
484
81
|
const sessionKey = resolveSessionKey(request, headers);
|
|
485
82
|
const sessionName = sessionNameFromKey(sessionKey);
|
|
83
|
+
// Diagnostic: privacy-safe request-shape logger (CC_OPENCLAW_REQ_SHAPE_LOG=1).
|
|
84
|
+
// Logs only kind+length+part-key metadata, never content text. No-op when env unset.
|
|
85
|
+
if (process.env.CC_OPENCLAW_REQ_SHAPE_LOG === '1') {
|
|
86
|
+
logReqShape(request, sessionName);
|
|
87
|
+
}
|
|
486
88
|
const isStreaming = request.stream === true;
|
|
487
89
|
// Pillar B v0.4.1: emit request_in trajectory event. No-op when
|
|
488
90
|
// CC_OPENCLAW_TRAJECTORY env flag is unset. _t0 captured for response_complete latency.
|
|
@@ -699,396 +301,10 @@ export async function handleChatCompletion(manager, body, headers, res) {
|
|
|
699
301
|
manager.stopSession(sessionName).catch(() => { });
|
|
700
302
|
}
|
|
701
303
|
}
|
|
702
|
-
//
|
|
703
|
-
//
|
|
704
|
-
//
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
* status updates when the CLI agent uses tools, so an external dashboard (e.g.
|
|
709
|
-
* a webchat status bar) can show real-time progress.
|
|
710
|
-
*
|
|
711
|
-
* Example: `OPENAI_COMPAT_STATUS_URL=http://127.0.0.1:18795/my-app/agent-status`
|
|
712
|
-
*/
|
|
713
|
-
function reportStatus(state, activity, tool) {
|
|
714
|
-
const url = getOpenaiCompatStatusUrl();
|
|
715
|
-
if (!url)
|
|
716
|
-
return;
|
|
717
|
-
const payload = JSON.stringify({ state, activity, tool: tool || null });
|
|
718
|
-
const req = http.request(url, {
|
|
719
|
-
method: 'POST',
|
|
720
|
-
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) },
|
|
721
|
-
timeout: 2000,
|
|
722
|
-
}, () => { });
|
|
723
|
-
req.on('error', () => { });
|
|
724
|
-
req.write(payload);
|
|
725
|
-
req.end();
|
|
726
|
-
}
|
|
727
|
-
function getToolDescription(toolName, toolInput) {
|
|
728
|
-
switch (toolName) {
|
|
729
|
-
case 'Bash':
|
|
730
|
-
case 'exec': {
|
|
731
|
-
const cmd = String(toolInput?.command || '');
|
|
732
|
-
return `Running: ${cmd.length > 50 ? cmd.slice(0, 50) + '...' : cmd}`;
|
|
733
|
-
}
|
|
734
|
-
case 'Read':
|
|
735
|
-
case 'read':
|
|
736
|
-
return `Reading: ${String(toolInput?.file_path || toolInput?.path || 'file')
|
|
737
|
-
.split('/')
|
|
738
|
-
.pop()}`;
|
|
739
|
-
case 'Write':
|
|
740
|
-
case 'write':
|
|
741
|
-
return `Writing: ${String(toolInput?.file_path || toolInput?.path || 'file')
|
|
742
|
-
.split('/')
|
|
743
|
-
.pop()}`;
|
|
744
|
-
case 'Edit':
|
|
745
|
-
case 'edit':
|
|
746
|
-
return `Editing: ${String(toolInput?.file_path || toolInput?.path || 'file')
|
|
747
|
-
.split('/')
|
|
748
|
-
.pop()}`;
|
|
749
|
-
case 'Glob':
|
|
750
|
-
case 'glob':
|
|
751
|
-
return `Searching files: ${String(toolInput?.pattern || '')}`;
|
|
752
|
-
case 'Grep':
|
|
753
|
-
case 'grep':
|
|
754
|
-
return `Searching content: ${String(toolInput?.pattern || '')}`;
|
|
755
|
-
case 'WebSearch':
|
|
756
|
-
return `Web search: ${String(toolInput?.query || '')}`;
|
|
757
|
-
case 'Agent':
|
|
758
|
-
return `Spawning sub-agent...`;
|
|
759
|
-
default:
|
|
760
|
-
return `Using tool: ${toolName}`;
|
|
761
|
-
}
|
|
762
|
-
}
|
|
763
|
-
// ─── Non-Streaming ───────────────────────────────────────────────────────────
|
|
764
|
-
async function handleNonStreaming(manager, sessionName, model,
|
|
765
|
-
// Phase 2 R4 wire-up: accepts native content-block arrays in tool-stream mode.
|
|
766
|
-
userMessage, completionId, res, hasTools) {
|
|
767
|
-
try {
|
|
768
|
-
reportStatus('thinking', 'Processing request...');
|
|
769
|
-
// v0.7.1: accumulate thinking-block content when surfaceThinking is on.
|
|
770
|
-
// Default OFF for privacy — empty string means no `reasoning` field
|
|
771
|
-
// gets attached to the response.
|
|
772
|
-
const surfaceThinking = getSurfaceThinkingEnabled();
|
|
773
|
-
let thinkingBuffer = '';
|
|
774
|
-
const result = await manager.sendMessage(sessionName, userMessage, {
|
|
775
|
-
onEvent: (event) => {
|
|
776
|
-
if (event.type === 'tool_use' && event.tool?.name) {
|
|
777
|
-
const desc = getToolDescription(event.tool.name, event.tool.input);
|
|
778
|
-
reportStatus('working', desc, event.tool.name);
|
|
779
|
-
// Pillar B v0.4.3: trajectory tool_use event. Emit tool name and
|
|
780
|
-
// input-arg keys (not values — keys leak no sensitive content
|
|
781
|
-
// while still letting offline analysis cluster tool-call shapes).
|
|
782
|
-
emitTrajectory('tool_use', {
|
|
783
|
-
name: event.tool.name,
|
|
784
|
-
inputKeys: event.tool.input ? Object.keys(event.tool.input) : [],
|
|
785
|
-
}, sessionName);
|
|
786
|
-
}
|
|
787
|
-
else if (event.type === 'tool_result') {
|
|
788
|
-
emitTrajectory('tool_result', {}, sessionName);
|
|
789
|
-
}
|
|
790
|
-
},
|
|
791
|
-
// v0.7.1: when surfaceThinking is on, accumulate extended-thinking text
|
|
792
|
-
// for the `reasoning` field on the OpenAI response. Subscribing to the
|
|
793
|
-
// callback always (cheap closure cost ~ none); only buffering when
|
|
794
|
-
// the env flag is set so the privacy-default-OFF promise holds.
|
|
795
|
-
onThinking: surfaceThinking
|
|
796
|
-
? (text) => {
|
|
797
|
-
thinkingBuffer += text;
|
|
798
|
-
}
|
|
799
|
-
: undefined,
|
|
800
|
-
});
|
|
801
|
-
reportStatus('idle', 'Ready');
|
|
802
|
-
let tokensIn = 0;
|
|
803
|
-
let tokensOut = 0;
|
|
804
|
-
try {
|
|
805
|
-
const status = manager.getStatus(sessionName);
|
|
806
|
-
tokensIn = status.stats.tokensIn;
|
|
807
|
-
tokensOut = status.stats.tokensOut;
|
|
808
|
-
}
|
|
809
|
-
catch {
|
|
810
|
-
/* stats unavailable */
|
|
811
|
-
}
|
|
812
|
-
// v0.7.1: emit thinking_block trajectory event with token-count metadata
|
|
813
|
-
// only (never raw text). Fires when buffer is non-empty regardless of
|
|
814
|
-
// whether the response surfaces it — so observability is independent
|
|
815
|
-
// of the user-visible flag.
|
|
816
|
-
if (thinkingBuffer.length > 0) {
|
|
817
|
-
emitTrajectory('thinking_block', {
|
|
818
|
-
excerpt_chars: thinkingBuffer.length,
|
|
819
|
-
tokens_approx: Math.ceil(thinkingBuffer.length / 4),
|
|
820
|
-
}, sessionName);
|
|
821
|
-
}
|
|
822
|
-
// Parse tool_calls from response text when caller provided tools
|
|
823
|
-
if (hasTools) {
|
|
824
|
-
const parsed = parseToolCallsFromText(result.output);
|
|
825
|
-
const response = formatCompletionResponse(completionId, model, parsed.textContent ?? '', tokensIn, tokensOut, parsed.toolCalls.length > 0 ? parsed.toolCalls : undefined, surfaceThinking ? thinkingBuffer : undefined);
|
|
826
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
827
|
-
res.end(JSON.stringify(response));
|
|
828
|
-
}
|
|
829
|
-
else {
|
|
830
|
-
const response = formatCompletionResponse(completionId, model, result.output, tokensIn, tokensOut, undefined, surfaceThinking ? thinkingBuffer : undefined);
|
|
831
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
832
|
-
res.end(JSON.stringify(response));
|
|
833
|
-
}
|
|
834
|
-
}
|
|
835
|
-
catch (err) {
|
|
836
|
-
reportStatus('idle', 'Request failed');
|
|
837
|
-
// v0.4.3: route through formatError for errors_total + trajectory error.
|
|
838
|
-
formatError(err, { code: ERROR_CODES.SESSION_ERROR, sessionId: sessionName, details: { phase: 'handleNonStreaming' } });
|
|
839
|
-
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
840
|
-
res.end(JSON.stringify({ error: { message: err.message, type: 'server_error' } }));
|
|
841
|
-
}
|
|
842
|
-
}
|
|
843
|
-
// ─── Streaming ───────────────────────────────────────────────────────────────
|
|
844
|
-
async function handleStreaming(manager, sessionName, model,
|
|
845
|
-
// Phase 2 R4 wire-up: accepts native content-block arrays in tool-stream mode.
|
|
846
|
-
userMessage, completionId, res, hasTools) {
|
|
847
|
-
res.writeHead(200, {
|
|
848
|
-
'Content-Type': 'text/event-stream',
|
|
849
|
-
'Cache-Control': 'no-cache',
|
|
850
|
-
Connection: 'keep-alive',
|
|
851
|
-
'X-Accel-Buffering': 'no',
|
|
852
|
-
});
|
|
853
|
-
let clientDisconnected = false;
|
|
854
|
-
res.on('close', () => {
|
|
855
|
-
clientDisconnected = true;
|
|
856
|
-
});
|
|
857
|
-
const writeSSE = (data) => {
|
|
858
|
-
if (!clientDisconnected) {
|
|
859
|
-
try {
|
|
860
|
-
res.write(`data: ${data}\n\n`);
|
|
861
|
-
}
|
|
862
|
-
catch {
|
|
863
|
-
clientDisconnected = true;
|
|
864
|
-
}
|
|
865
|
-
}
|
|
866
|
-
};
|
|
867
|
-
// Initial chunk with role
|
|
868
|
-
writeSSE(JSON.stringify(formatCompletionChunk(completionId, model, { role: 'assistant' }, null)));
|
|
869
|
-
// SSE keepalive heartbeat
|
|
870
|
-
const heartbeatTimer = setInterval(() => {
|
|
871
|
-
if (!clientDisconnected) {
|
|
872
|
-
try {
|
|
873
|
-
res.write(': keepalive\n\n');
|
|
874
|
-
}
|
|
875
|
-
catch {
|
|
876
|
-
clientDisconnected = true;
|
|
877
|
-
}
|
|
878
|
-
}
|
|
879
|
-
}, 30_000);
|
|
880
|
-
// Phase 2 R1+R2: in tool-stream mode, bridge session-manager's pre-parsed
|
|
881
|
-
// tool_use events directly to OpenAI tool_calls SSE deltas. Skips the
|
|
882
|
-
// legacy "buffer text + regex-parse <tool_calls> XML" path entirely.
|
|
883
|
-
// Per memory project_cc_openclaw_session_manager_preparses.md:
|
|
884
|
-
// session-manager has already stripped Claude CLI's NDJSON envelope, so
|
|
885
|
-
// we don't need cli-stream-parser here — onEvent is the parser output.
|
|
886
|
-
const useToolStream = isToolStreamMode() && hasTools;
|
|
887
|
-
// When tools are present (legacy mode), buffer the full response to parse
|
|
888
|
-
// for <tool_calls> XML. Without tools — or in tool-stream mode — stream
|
|
889
|
-
// text chunks directly for low latency.
|
|
890
|
-
let bufferedText = '';
|
|
891
|
-
let toolCallsEmitted = 0;
|
|
892
|
-
// v0.7.2 streaming-path backstop: track whether *any* visible content
|
|
893
|
-
// (text chunk OR tool_calls SSE chunk) was ever streamed. If the model
|
|
894
|
-
// uses only CLI built-in tools (Bash/Read/Write) without producing text,
|
|
895
|
-
// bufferedText stays empty AND no caller-visible tool_calls get streamed,
|
|
896
|
-
// resulting in zero content payloads — which OpenClaw upstream's
|
|
897
|
-
// result-fallback-classifier rejects as an "incomplete terminal response".
|
|
898
|
-
// This flag drives a final-chunk backstop in each finalization branch.
|
|
899
|
-
let streamedAnything = false;
|
|
900
|
-
try {
|
|
901
|
-
reportStatus('thinking', 'Processing request...');
|
|
902
|
-
await manager.sendMessage(sessionName, userMessage, {
|
|
903
|
-
onChunk: (chunk) => {
|
|
904
|
-
if (useToolStream || !hasTools) {
|
|
905
|
-
// Stream text deltas immediately. Tool-stream mode interleaves
|
|
906
|
-
// text and tool_calls chunks naturally — Claude CLI emits text
|
|
907
|
-
// between tool_use blocks, OpenClaw client handles that fine.
|
|
908
|
-
if (chunk.length > 0)
|
|
909
|
-
streamedAnything = true;
|
|
910
|
-
writeSSE(JSON.stringify(formatCompletionChunk(completionId, model, { content: chunk }, null)));
|
|
911
|
-
}
|
|
912
|
-
else {
|
|
913
|
-
// Legacy hasTools mode: buffer for XML <tool_calls> parsing post-stream.
|
|
914
|
-
bufferedText += chunk;
|
|
915
|
-
}
|
|
916
|
-
},
|
|
917
|
-
onEvent: (event) => {
|
|
918
|
-
if (event.type === 'tool_result') {
|
|
919
|
-
// Pillar B v0.4.3: streaming tool_result trajectory event.
|
|
920
|
-
emitTrajectory('tool_result', {}, sessionName);
|
|
921
|
-
return;
|
|
922
|
-
}
|
|
923
|
-
if (event.type === 'tool_use' && event.tool?.name) {
|
|
924
|
-
reportStatus('working', getToolDescription(event.tool.name, event.tool.input), event.tool.name);
|
|
925
|
-
// Pillar B v0.4.3: streaming tool_use trajectory event. Same
|
|
926
|
-
// privacy-preserving inputKeys-only payload as handleNonStreaming.
|
|
927
|
-
emitTrajectory('tool_use', {
|
|
928
|
-
name: event.tool.name,
|
|
929
|
-
inputKeys: event.tool.input ? Object.keys(event.tool.input) : [],
|
|
930
|
-
}, sessionName);
|
|
931
|
-
if (useToolStream) {
|
|
932
|
-
// R1+R2 bridge: session-manager event → OpenAI tool_calls SSE.
|
|
933
|
-
// Emit two chunks per tool_use (per OpenAI streaming spec):
|
|
934
|
-
// 1. id + name + empty arguments
|
|
935
|
-
// 2. arguments (JSON-stringified input)
|
|
936
|
-
const toolUseId = event.tool.id ||
|
|
937
|
-
`toolu_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
|
938
|
-
const idx = toolCallsEmitted;
|
|
939
|
-
const argsJson = event.tool.input != null ? JSON.stringify(event.tool.input) : '{}';
|
|
940
|
-
const startChunk = {
|
|
941
|
-
id: completionId,
|
|
942
|
-
object: 'chat.completion.chunk',
|
|
943
|
-
created: Math.floor(Date.now() / 1000),
|
|
944
|
-
model,
|
|
945
|
-
choices: [
|
|
946
|
-
{
|
|
947
|
-
index: 0,
|
|
948
|
-
delta: {
|
|
949
|
-
tool_calls: [
|
|
950
|
-
{
|
|
951
|
-
index: idx,
|
|
952
|
-
id: toolUseId,
|
|
953
|
-
type: 'function',
|
|
954
|
-
function: { name: event.tool.name, arguments: '' },
|
|
955
|
-
},
|
|
956
|
-
],
|
|
957
|
-
},
|
|
958
|
-
finish_reason: null,
|
|
959
|
-
},
|
|
960
|
-
],
|
|
961
|
-
};
|
|
962
|
-
const argsChunk = {
|
|
963
|
-
id: completionId,
|
|
964
|
-
object: 'chat.completion.chunk',
|
|
965
|
-
created: Math.floor(Date.now() / 1000),
|
|
966
|
-
model,
|
|
967
|
-
choices: [
|
|
968
|
-
{
|
|
969
|
-
index: 0,
|
|
970
|
-
delta: {
|
|
971
|
-
tool_calls: [
|
|
972
|
-
{
|
|
973
|
-
index: idx,
|
|
974
|
-
function: { arguments: argsJson },
|
|
975
|
-
},
|
|
976
|
-
],
|
|
977
|
-
},
|
|
978
|
-
finish_reason: null,
|
|
979
|
-
},
|
|
980
|
-
],
|
|
981
|
-
};
|
|
982
|
-
writeSSE(JSON.stringify(startChunk));
|
|
983
|
-
writeSSE(JSON.stringify(argsChunk));
|
|
984
|
-
toolCallsEmitted += 1;
|
|
985
|
-
streamedAnything = true;
|
|
986
|
-
}
|
|
987
|
-
}
|
|
988
|
-
},
|
|
989
|
-
});
|
|
990
|
-
reportStatus('idle', 'Ready');
|
|
991
|
-
// Get token usage for final chunk
|
|
992
|
-
let usage;
|
|
993
|
-
try {
|
|
994
|
-
const status = manager.getStatus(sessionName);
|
|
995
|
-
usage = {
|
|
996
|
-
prompt_tokens: status.stats.tokensIn,
|
|
997
|
-
completion_tokens: status.stats.tokensOut,
|
|
998
|
-
total_tokens: status.stats.tokensIn + status.stats.tokensOut,
|
|
999
|
-
};
|
|
1000
|
-
}
|
|
1001
|
-
catch {
|
|
1002
|
-
/* best effort */
|
|
1003
|
-
}
|
|
1004
|
-
// v0.7.2 streaming-path backstop: if nothing visible was streamed AND
|
|
1005
|
-
// bufferedText (legacy mode) is also empty, emit a minimal "Done." text
|
|
1006
|
-
// chunk before the finish chunk so the upstream classifier sees a
|
|
1007
|
-
// payload. Skip when tool_calls were emitted — those are openai-spec
|
|
1008
|
-
// valid as the only payload (multi-turn tool-use sessions).
|
|
1009
|
-
const noVisiblePayload = !streamedAnything && bufferedText.length === 0 && toolCallsEmitted === 0;
|
|
1010
|
-
if (noVisiblePayload) {
|
|
1011
|
-
writeSSE(JSON.stringify(formatCompletionChunk(completionId, model, { content: 'Done.' }, null)));
|
|
1012
|
-
streamedAnything = true;
|
|
1013
|
-
}
|
|
1014
|
-
if (useToolStream) {
|
|
1015
|
-
// R1+R2: tool-stream mode — text + tool_calls already streamed inline.
|
|
1016
|
-
// Just emit the final chunk with the right finish_reason.
|
|
1017
|
-
const finishReason = toolCallsEmitted > 0 ? 'tool_calls' : 'stop';
|
|
1018
|
-
const finalChunk = formatCompletionChunk(completionId, model, {}, finishReason);
|
|
1019
|
-
if (usage)
|
|
1020
|
-
finalChunk.usage = usage;
|
|
1021
|
-
writeSSE(JSON.stringify(finalChunk));
|
|
1022
|
-
}
|
|
1023
|
-
else if (hasTools && bufferedText) {
|
|
1024
|
-
const parsed = parseToolCallsFromText(bufferedText);
|
|
1025
|
-
if (parsed.toolCalls.length > 0) {
|
|
1026
|
-
// Emit text content if any
|
|
1027
|
-
if (parsed.textContent) {
|
|
1028
|
-
writeSSE(JSON.stringify(formatCompletionChunk(completionId, model, { content: parsed.textContent }, null)));
|
|
1029
|
-
}
|
|
1030
|
-
// Emit tool_call chunks
|
|
1031
|
-
for (let i = 0; i < parsed.toolCalls.length; i++) {
|
|
1032
|
-
const tc = parsed.toolCalls[i];
|
|
1033
|
-
writeSSE(JSON.stringify({
|
|
1034
|
-
id: completionId,
|
|
1035
|
-
object: 'chat.completion.chunk',
|
|
1036
|
-
created: Math.floor(Date.now() / 1000),
|
|
1037
|
-
model,
|
|
1038
|
-
choices: [
|
|
1039
|
-
{
|
|
1040
|
-
index: 0,
|
|
1041
|
-
delta: {
|
|
1042
|
-
tool_calls: [
|
|
1043
|
-
{
|
|
1044
|
-
index: i,
|
|
1045
|
-
id: tc.id,
|
|
1046
|
-
type: 'function',
|
|
1047
|
-
function: { name: tc.function.name, arguments: tc.function.arguments },
|
|
1048
|
-
},
|
|
1049
|
-
],
|
|
1050
|
-
},
|
|
1051
|
-
finish_reason: null,
|
|
1052
|
-
},
|
|
1053
|
-
],
|
|
1054
|
-
}));
|
|
1055
|
-
}
|
|
1056
|
-
// Final chunk with tool_calls finish reason
|
|
1057
|
-
const finalChunk = formatCompletionChunk(completionId, model, {}, 'tool_calls');
|
|
1058
|
-
if (usage)
|
|
1059
|
-
finalChunk.usage = usage;
|
|
1060
|
-
writeSSE(JSON.stringify(finalChunk));
|
|
1061
|
-
}
|
|
1062
|
-
else {
|
|
1063
|
-
// No tool calls — emit buffered text as content
|
|
1064
|
-
writeSSE(JSON.stringify(formatCompletionChunk(completionId, model, { content: bufferedText }, null)));
|
|
1065
|
-
const finalChunk = formatCompletionChunk(completionId, model, {}, 'stop');
|
|
1066
|
-
if (usage)
|
|
1067
|
-
finalChunk.usage = usage;
|
|
1068
|
-
writeSSE(JSON.stringify(finalChunk));
|
|
1069
|
-
}
|
|
1070
|
-
}
|
|
1071
|
-
else {
|
|
1072
|
-
// No tools — standard finish
|
|
1073
|
-
const finalChunk = formatCompletionChunk(completionId, model, {}, 'stop');
|
|
1074
|
-
if (usage)
|
|
1075
|
-
finalChunk.usage = usage;
|
|
1076
|
-
writeSSE(JSON.stringify(finalChunk));
|
|
1077
|
-
}
|
|
1078
|
-
writeSSE('[DONE]');
|
|
1079
|
-
}
|
|
1080
|
-
catch (err) {
|
|
1081
|
-
reportStatus('idle', 'Request failed');
|
|
1082
|
-
// v0.4.3: route through formatError for errors_total + trajectory error.
|
|
1083
|
-
formatError(err, { code: ERROR_CODES.SESSION_ERROR, sessionId: sessionName, details: { phase: 'handleStreaming' } });
|
|
1084
|
-
writeSSE(JSON.stringify({ error: { message: err.message, type: 'server_error' } }));
|
|
1085
|
-
writeSSE('[DONE]');
|
|
1086
|
-
}
|
|
1087
|
-
finally {
|
|
1088
|
-
clearInterval(heartbeatTimer);
|
|
1089
|
-
}
|
|
1090
|
-
if (!clientDisconnected) {
|
|
1091
|
-
res.end();
|
|
1092
|
-
}
|
|
1093
|
-
}
|
|
304
|
+
// reportStatus + getToolDescription moved to status-reporter.ts
|
|
305
|
+
// (Cluster B Phase 2 Module F). Re-exported above for backward compat.
|
|
306
|
+
// handleNonStreaming moved to non-streaming-handler.ts
|
|
307
|
+
// (Cluster B Phase 2 Module G). Re-exported above for backward compat.
|
|
308
|
+
// handleStreaming moved to streaming-handler.ts
|
|
309
|
+
// (Cluster B Phase 2 Module H). Re-exported above for backward compat.
|
|
1094
310
|
//# sourceMappingURL=openai-compat.js.map
|