diff --git a/api/chat-stream.js b/api/chat-stream.js index 32e7601..9241b04 100644 --- a/api/chat-stream.js +++ b/api/chat-stream.js @@ -1,3 +1,3 @@ 'use strict'; -module.exports = require('./chat-stream/index.js'); +module.exports = require('../internal/js/chat-stream/index.js'); diff --git a/internal/adapter/openai/responses_stream_runtime_toolcalls.go b/internal/adapter/openai/responses_stream_runtime_toolcalls.go index 052e865..9947cbd 100644 --- a/internal/adapter/openai/responses_stream_runtime_toolcalls.go +++ b/internal/adapter/openai/responses_stream_runtime_toolcalls.go @@ -2,7 +2,6 @@ package openai import ( "encoding/json" - "sort" "strings" openaifmt "ds2api/internal/format/openai" @@ -234,149 +233,3 @@ func (s *responsesStreamRuntime) emitFunctionCallDoneEvents(calls []util.ParsedT s.toolCallsDoneEmitted = true } } - -func (s *responsesStreamRuntime) closeIncompleteFunctionItems() { - if len(s.functionAdded) == 0 { - return - } - indices := make([]int, 0, len(s.functionAdded)) - for idx, added := range s.functionAdded { - if !added || s.functionDone[idx] { - continue - } - indices = append(indices, idx) - } - if len(indices) == 0 { - return - } - sort.Ints(indices) - for _, idx := range indices { - name := strings.TrimSpace(s.functionNames[idx]) - if name == "" { - continue - } - args := strings.TrimSpace(s.functionArgs[idx]) - if args == "" { - args = "{}" - } - outputIndex := s.ensureFunctionOutputIndex(idx) - itemID := s.ensureFunctionItemID(idx) - callID := s.ensureToolCallID(idx) - s.sendEvent( - "response.function_call_arguments.done", - openaifmt.BuildResponsesFunctionCallArgumentsDonePayload(s.responseID, itemID, outputIndex, callID, name, args), - ) - item := map[string]any{ - "id": itemID, - "type": "function_call", - "call_id": callID, - "name": name, - "arguments": args, - "status": "completed", - } - s.sendEvent( - "response.output_item.done", - openaifmt.BuildResponsesOutputItemDonePayload(s.responseID, itemID, outputIndex, item), - ) - s.functionDone[idx] = true - s.toolCallsDoneEmitted = true - } -} - -func (s *responsesStreamRuntime) buildCompletedResponseObject(finalThinking, finalText string, calls []util.ParsedToolCall) map[string]any { - type indexedItem struct { - index int - item map[string]any - } - indexed := make([]indexedItem, 0, len(calls)+1) - - if s.messageAdded { - text := s.visibleText.String() - indexed = append(indexed, indexedItem{ - index: s.ensureMessageOutputIndex(), - item: map[string]any{ - "id": s.ensureMessageItemID(), - "type": "message", - "role": "assistant", - "status": "completed", - "content": []map[string]any{ - { - "type": "output_text", - "text": text, - }, - }, - }, - }) - } else if len(calls) == 0 { - content := make([]map[string]any, 0, 2) - if strings.TrimSpace(finalThinking) != "" { - content = append(content, map[string]any{ - "type": "reasoning", - "text": finalThinking, - }) - } - if strings.TrimSpace(finalText) != "" { - content = append(content, map[string]any{ - "type": "output_text", - "text": finalText, - }) - } - if len(content) > 0 { - indexed = append(indexed, indexedItem{ - index: s.ensureMessageOutputIndex(), - item: map[string]any{ - "id": s.ensureMessageItemID(), - "type": "message", - "role": "assistant", - "status": "completed", - "content": content, - }, - }) - } - } - - for idx, tc := range calls { - if strings.TrimSpace(tc.Name) == "" { - continue - } - argsBytes, _ := json.Marshal(tc.Input) - indexed = append(indexed, indexedItem{ - index: s.ensureFunctionOutputIndex(idx), - item: map[string]any{ - "id": s.ensureFunctionItemID(idx), - "type": "function_call", - "call_id": s.ensureToolCallID(idx), - "name": tc.Name, - "arguments": string(argsBytes), - "status": "completed", - }, - }) - } - - sort.SliceStable(indexed, func(i, j int) bool { - return indexed[i].index < indexed[j].index - }) - output := make([]any, 0, len(indexed)) - for _, it := range indexed { - output = append(output, it.item) - } - - outputText := s.visibleText.String() - if strings.TrimSpace(outputText) == "" && len(calls) == 0 { - if strings.TrimSpace(finalText) != "" { - outputText = finalText - } else if strings.TrimSpace(finalThinking) != "" { - outputText = finalThinking - } - } - - return openaifmt.BuildResponseObjectFromItems( - s.responseID, - s.model, - s.finalPrompt, - finalThinking, - finalText, - output, - outputText, - ) -} diff --git a/internal/adapter/openai/responses_stream_runtime_toolcalls_finalize.go b/internal/adapter/openai/responses_stream_runtime_toolcalls_finalize.go new file mode 100644 index 0000000..46104a1 --- /dev/null +++ b/internal/adapter/openai/responses_stream_runtime_toolcalls_finalize.go @@ -0,0 +1,156 @@ +package openai + +import ( + "encoding/json" + "sort" + "strings" + + openaifmt "ds2api/internal/format/openai" + "ds2api/internal/util" +) + +func (s *responsesStreamRuntime) closeIncompleteFunctionItems() { + if len(s.functionAdded) == 0 { + return + } + indices := make([]int, 0, len(s.functionAdded)) + for idx, added := range s.functionAdded { + if !added || s.functionDone[idx] { + continue + } + indices = append(indices, idx) + } + if len(indices) == 0 { + return + } + sort.Ints(indices) + for _, idx := range indices { + name := strings.TrimSpace(s.functionNames[idx]) + if name == "" { + continue + } + args := strings.TrimSpace(s.functionArgs[idx]) + if args == "" { + args = "{}" + } + outputIndex := s.ensureFunctionOutputIndex(idx) + itemID := s.ensureFunctionItemID(idx) + callID := s.ensureToolCallID(idx) + s.sendEvent( + "response.function_call_arguments.done", + openaifmt.BuildResponsesFunctionCallArgumentsDonePayload(s.responseID, itemID, outputIndex, callID, name, args), + ) + item := map[string]any{ + "id": itemID, + "type": "function_call", + "call_id": callID, + "name": name, + "arguments": args, + "status": "completed", + } + s.sendEvent( + "response.output_item.done", + openaifmt.BuildResponsesOutputItemDonePayload(s.responseID, itemID, outputIndex, item), + ) + s.functionDone[idx] = true + s.toolCallsDoneEmitted = true + } +} + +func (s *responsesStreamRuntime) buildCompletedResponseObject(finalThinking, finalText string, calls []util.ParsedToolCall) map[string]any { + type indexedItem struct { + index int + item map[string]any + } + indexed := make([]indexedItem, 0, len(calls)+1) + + if s.messageAdded { + text := s.visibleText.String() + indexed = append(indexed, indexedItem{ + index: s.ensureMessageOutputIndex(), + item: map[string]any{ + "id": s.ensureMessageItemID(), + "type": "message", + "role": "assistant", + "status": "completed", + "content": []map[string]any{ + { + "type": "output_text", + "text": text, + }, + }, + }, + }) + } else if len(calls) == 0 { + content := make([]map[string]any, 0, 2) + if strings.TrimSpace(finalThinking) != "" { + content = append(content, map[string]any{ + "type": "reasoning", + "text": finalThinking, + }) + } + if strings.TrimSpace(finalText) != "" { + content = append(content, map[string]any{ + "type": "output_text", + "text": finalText, + }) + } + if len(content) > 0 { + indexed = append(indexed, indexedItem{ + index: s.ensureMessageOutputIndex(), + item: map[string]any{ + "id": s.ensureMessageItemID(), + "type": "message", + "role": "assistant", + "status": "completed", + "content": content, + }, + }) + } + } + + for idx, tc := range calls { + if strings.TrimSpace(tc.Name) == "" { + continue + } + argsBytes, _ := json.Marshal(tc.Input) + indexed = append(indexed, indexedItem{ + index: s.ensureFunctionOutputIndex(idx), + item: map[string]any{ + "id": s.ensureFunctionItemID(idx), + "type": "function_call", + "call_id": s.ensureToolCallID(idx), + "name": tc.Name, + "arguments": string(argsBytes), + "status": "completed", + }, + }) + } + + sort.SliceStable(indexed, func(i, j int) bool { + return indexed[i].index < indexed[j].index + }) + output := make([]any, 0, len(indexed)) + for _, it := range indexed { + output = append(output, it.item) + } + + outputText := s.visibleText.String() + if strings.TrimSpace(outputText) == "" && len(calls) == 0 { + if strings.TrimSpace(finalText) != "" { + outputText = finalText + } else if strings.TrimSpace(finalThinking) != "" { + outputText = finalThinking + } + } + + return openaifmt.BuildResponseObjectFromItems( + s.responseID, + s.model, + s.finalPrompt, + finalThinking, + finalText, + output, + outputText, + ) +} diff --git a/api/chat-stream/error_shape.js b/internal/js/chat-stream/error_shape.js similarity index 100% rename from api/chat-stream/error_shape.js rename to internal/js/chat-stream/error_shape.js diff --git a/api/chat-stream/http_internal.js b/internal/js/chat-stream/http_internal.js similarity index 100% rename from api/chat-stream/http_internal.js rename to internal/js/chat-stream/http_internal.js diff --git a/api/chat-stream/index.js b/internal/js/chat-stream/index.js similarity index 100% rename from api/chat-stream/index.js rename to internal/js/chat-stream/index.js diff --git a/api/chat-stream/proxy_go.js b/internal/js/chat-stream/proxy_go.js similarity index 100% rename from api/chat-stream/proxy_go.js rename to internal/js/chat-stream/proxy_go.js diff --git a/api/chat-stream/sse_parse.js b/internal/js/chat-stream/sse_parse.js similarity index 100% rename from api/chat-stream/sse_parse.js rename to internal/js/chat-stream/sse_parse.js diff --git a/api/chat-stream/stream_emitter.js b/internal/js/chat-stream/stream_emitter.js similarity index 100% rename from api/chat-stream/stream_emitter.js rename to internal/js/chat-stream/stream_emitter.js diff --git a/api/chat-stream/token_usage.js b/internal/js/chat-stream/token_usage.js similarity index 100% rename from api/chat-stream/token_usage.js rename to internal/js/chat-stream/token_usage.js diff --git a/api/chat-stream/toolcall_policy.js b/internal/js/chat-stream/toolcall_policy.js similarity index 100% rename from api/chat-stream/toolcall_policy.js rename to internal/js/chat-stream/toolcall_policy.js diff --git a/api/chat-stream/vercel_stream.js b/internal/js/chat-stream/vercel_stream.js similarity index 100% rename from api/chat-stream/vercel_stream.js rename to internal/js/chat-stream/vercel_stream.js diff --git a/api/helpers/stream-tool-sieve.js b/internal/js/helpers/stream-tool-sieve.js similarity index 100% rename from api/helpers/stream-tool-sieve.js rename to internal/js/helpers/stream-tool-sieve.js diff --git a/api/helpers/stream-tool-sieve/format.js b/internal/js/helpers/stream-tool-sieve/format.js similarity index 100% rename from api/helpers/stream-tool-sieve/format.js rename to internal/js/helpers/stream-tool-sieve/format.js diff --git a/api/helpers/stream-tool-sieve/incremental.js b/internal/js/helpers/stream-tool-sieve/incremental.js similarity index 100% rename from api/helpers/stream-tool-sieve/incremental.js rename to internal/js/helpers/stream-tool-sieve/incremental.js diff --git a/api/helpers/stream-tool-sieve/index.js b/internal/js/helpers/stream-tool-sieve/index.js similarity index 100% rename from api/helpers/stream-tool-sieve/index.js rename to internal/js/helpers/stream-tool-sieve/index.js diff --git a/api/helpers/stream-tool-sieve/jsonscan.js b/internal/js/helpers/stream-tool-sieve/jsonscan.js similarity index 100% rename from api/helpers/stream-tool-sieve/jsonscan.js rename to internal/js/helpers/stream-tool-sieve/jsonscan.js diff --git a/api/helpers/stream-tool-sieve/parse.js b/internal/js/helpers/stream-tool-sieve/parse.js similarity index 100% rename from api/helpers/stream-tool-sieve/parse.js rename to internal/js/helpers/stream-tool-sieve/parse.js diff --git a/api/helpers/stream-tool-sieve/sieve.js b/internal/js/helpers/stream-tool-sieve/sieve.js similarity index 100% rename from api/helpers/stream-tool-sieve/sieve.js rename to internal/js/helpers/stream-tool-sieve/sieve.js diff --git a/api/helpers/stream-tool-sieve/state.js b/internal/js/helpers/stream-tool-sieve/state.js similarity index 100% rename from api/helpers/stream-tool-sieve/state.js rename to internal/js/helpers/stream-tool-sieve/state.js diff --git a/api/shared/deepseek-constants.js b/internal/js/shared/deepseek-constants.js similarity index 100% rename from api/shared/deepseek-constants.js rename to internal/js/shared/deepseek-constants.js diff --git a/internal/testsuite/runner_env.go b/internal/testsuite/runner_env.go index 1ec6744..a953936 100644 --- a/internal/testsuite/runner_env.go +++ b/internal/testsuite/runner_env.go @@ -101,7 +101,7 @@ func preflightSteps() [][]string { return [][]string{ {"go", "test", "./...", "-count=1"}, {"./tests/scripts/check-node-split-syntax.sh"}, - {"node", "--test", "api/helpers/stream-tool-sieve.test.js", "api/chat-stream.test.js", "api/compat/js_compat_test.js"}, + {"node", "--test", "tests/node/stream-tool-sieve.test.js", "tests/node/chat-stream.test.js", "tests/node/js_compat_test.js"}, {"npm", "run", "build", "--prefix", "webui"}, } } diff --git a/internal/testsuite/runner_env_test.go b/internal/testsuite/runner_env_test.go index 0c28b37..98df72c 100644 --- a/internal/testsuite/runner_env_test.go +++ b/internal/testsuite/runner_env_test.go @@ -9,7 +9,7 @@ func TestPreflightStepsExactSequence(t *testing.T) { want := [][]string{ {"go", "test", "./...", "-count=1"}, {"./tests/scripts/check-node-split-syntax.sh"}, - {"node", "--test", "api/helpers/stream-tool-sieve.test.js", "api/chat-stream.test.js", "api/compat/js_compat_test.js"}, + {"node", "--test", "tests/node/stream-tool-sieve.test.js", "tests/node/chat-stream.test.js", "tests/node/js_compat_test.js"}, {"npm", "run", "build", "--prefix", "webui"}, } diff --git a/plans/node-syntax-gate-targets.txt b/plans/node-syntax-gate-targets.txt index 3d30111..7b268a8 100644 --- a/plans/node-syntax-gate-targets.txt +++ b/plans/node-syntax-gate-targets.txt @@ -1,22 +1,22 @@ # Node split syntax gate targets -# Keep this list in sync with api/chat-stream and api/helpers/stream-tool-sieve split modules. +# Keep this list in sync with api/chat-stream and internal/js/helpers/stream-tool-sieve split modules. api/chat-stream.js -api/chat-stream/index.js -api/chat-stream/error_shape.js -api/chat-stream/http_internal.js -api/chat-stream/proxy_go.js -api/chat-stream/sse_parse.js -api/chat-stream/stream_emitter.js -api/chat-stream/token_usage.js -api/chat-stream/toolcall_policy.js -api/chat-stream/vercel_stream.js +internal/js/chat-stream/index.js +internal/js/chat-stream/error_shape.js +internal/js/chat-stream/http_internal.js +internal/js/chat-stream/proxy_go.js +internal/js/chat-stream/sse_parse.js +internal/js/chat-stream/stream_emitter.js +internal/js/chat-stream/token_usage.js +internal/js/chat-stream/toolcall_policy.js +internal/js/chat-stream/vercel_stream.js -api/helpers/stream-tool-sieve.js -api/helpers/stream-tool-sieve/index.js -api/helpers/stream-tool-sieve/state.js -api/helpers/stream-tool-sieve/sieve.js -api/helpers/stream-tool-sieve/incremental.js -api/helpers/stream-tool-sieve/jsonscan.js -api/helpers/stream-tool-sieve/parse.js -api/helpers/stream-tool-sieve/format.js +internal/js/helpers/stream-tool-sieve.js +internal/js/helpers/stream-tool-sieve/index.js +internal/js/helpers/stream-tool-sieve/state.js +internal/js/helpers/stream-tool-sieve/sieve.js +internal/js/helpers/stream-tool-sieve/incremental.js +internal/js/helpers/stream-tool-sieve/jsonscan.js +internal/js/helpers/stream-tool-sieve/parse.js +internal/js/helpers/stream-tool-sieve/format.js diff --git a/plans/refactor-line-gate-targets.txt b/plans/refactor-line-gate-targets.txt index d3678ad..c9839b2 100644 --- a/plans/refactor-line-gate-targets.txt +++ b/plans/refactor-line-gate-targets.txt @@ -91,24 +91,24 @@ internal/testsuite/edge_cases_abort.go internal/testsuite/edge_cases_error_contract.go api/chat-stream.js -api/chat-stream/index.js -api/chat-stream/vercel_stream.js -api/chat-stream/proxy_go.js -api/chat-stream/sse_parse.js -api/chat-stream/http_internal.js -api/chat-stream/toolcall_policy.js -api/chat-stream/error_shape.js -api/chat-stream/token_usage.js -api/chat-stream/stream_emitter.js +internal/js/chat-stream/index.js +internal/js/chat-stream/vercel_stream.js +internal/js/chat-stream/proxy_go.js +internal/js/chat-stream/sse_parse.js +internal/js/chat-stream/http_internal.js +internal/js/chat-stream/toolcall_policy.js +internal/js/chat-stream/error_shape.js +internal/js/chat-stream/token_usage.js +internal/js/chat-stream/stream_emitter.js -api/helpers/stream-tool-sieve.js -api/helpers/stream-tool-sieve/index.js -api/helpers/stream-tool-sieve/state.js -api/helpers/stream-tool-sieve/sieve.js -api/helpers/stream-tool-sieve/incremental.js -api/helpers/stream-tool-sieve/jsonscan.js -api/helpers/stream-tool-sieve/parse.js -api/helpers/stream-tool-sieve/format.js +internal/js/helpers/stream-tool-sieve.js +internal/js/helpers/stream-tool-sieve/index.js +internal/js/helpers/stream-tool-sieve/state.js +internal/js/helpers/stream-tool-sieve/sieve.js +internal/js/helpers/stream-tool-sieve/incremental.js +internal/js/helpers/stream-tool-sieve/jsonscan.js +internal/js/helpers/stream-tool-sieve/parse.js +internal/js/helpers/stream-tool-sieve/format.js webui/src/App.jsx webui/src/app/AppRoutes.jsx diff --git a/api/chat-stream.test.js b/tests/node/chat-stream.test.js similarity index 97% rename from api/chat-stream.test.js rename to tests/node/chat-stream.test.js index 7424df2..e31afbe 100644 --- a/api/chat-stream.test.js +++ b/tests/node/chat-stream.test.js @@ -3,12 +3,12 @@ const test = require('node:test'); const assert = require('node:assert/strict'); -const handler = require('./chat-stream'); +const handler = require('../../api/chat-stream.js'); const { createToolSieveState, processToolSieveChunk, flushToolSieve, -} = require('./helpers/stream-tool-sieve'); +} = require('../../internal/js/helpers/stream-tool-sieve.js'); const { parseChunkForContent, diff --git a/api/compat/js_compat_test.js b/tests/node/js_compat_test.js similarity index 94% rename from api/compat/js_compat_test.js rename to tests/node/js_compat_test.js index 9b03b00..0029abe 100644 --- a/api/compat/js_compat_test.js +++ b/tests/node/js_compat_test.js @@ -5,8 +5,8 @@ const assert = require('node:assert/strict'); const fs = require('node:fs'); const path = require('node:path'); -const chatStream = require('../chat-stream'); -const { parseToolCalls } = require('../helpers/stream-tool-sieve'); +const chatStream = require('../../api/chat-stream.js'); +const { parseToolCalls } = require('../../internal/js/helpers/stream-tool-sieve.js'); const { parseChunkForContent, estimateTokens } = chatStream.__test; diff --git a/api/helpers/stream-tool-sieve.test.js b/tests/node/stream-tool-sieve.test.js similarity index 99% rename from api/helpers/stream-tool-sieve.test.js rename to tests/node/stream-tool-sieve.test.js index 7f532f1..e96716b 100644 --- a/api/helpers/stream-tool-sieve.test.js +++ b/tests/node/stream-tool-sieve.test.js @@ -10,7 +10,7 @@ const { flushToolSieve, parseToolCalls, parseStandaloneToolCalls, -} = require('./stream-tool-sieve'); +} = require('../../internal/js/helpers/stream-tool-sieve.js'); function runSieve(chunks, toolNames) { const state = createToolSieveState(); diff --git a/tests/scripts/check-refactor-line-gate.sh b/tests/scripts/check-refactor-line-gate.sh index 0568666..4118d15 100755 --- a/tests/scripts/check-refactor-line-gate.sh +++ b/tests/scripts/check-refactor-line-gate.sh @@ -10,7 +10,7 @@ ENTRY_MAX=120 is_entry_file() { case "$1" in api/chat-stream.js|\ - api/helpers/stream-tool-sieve.js|\ + internal/js/helpers/stream-tool-sieve.js|\ webui/src/App.jsx|\ webui/src/components/AccountManager.jsx|\ webui/src/components/ApiTester.jsx|\ diff --git a/tests/scripts/run-unit-node.sh b/tests/scripts/run-unit-node.sh index 0f11847..515a961 100755 --- a/tests/scripts/run-unit-node.sh +++ b/tests/scripts/run-unit-node.sh @@ -5,4 +5,4 @@ ROOT_DIR="$(cd "$(dirname "$0")/../.." && pwd)" cd "$ROOT_DIR" ./tests/scripts/check-node-split-syntax.sh -node --test api/helpers/stream-tool-sieve.test.js api/chat-stream.test.js api/compat/js_compat_test.js "$@" +node --test tests/node/stream-tool-sieve.test.js tests/node/chat-stream.test.js tests/node/js_compat_test.js "$@"