refactor: Relocate JavaScript source and Node.js test files to dedicated directories and extract OpenAI stream runtime tool call finalization logic.

This commit is contained in:
CJACK
2026-02-22 22:37:08 +08:00
parent d3b60edb6f
commit d4017b87c1
30 changed files with 201 additions and 192 deletions

View File

@@ -1,3 +1,3 @@
'use strict';
module.exports = require('./chat-stream/index.js');
module.exports = require('../internal/js/chat-stream/index.js');

View File

@@ -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,
)
}

View File

@@ -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,
)
}

View File

@@ -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"},
}
}

View File

@@ -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"},
}

View File

@@ -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

View File

@@ -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

View File

@@ -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,

View File

@@ -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;

View File

@@ -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();

View File

@@ -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|\

View File

@@ -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 "$@"