mirror of
https://github.com/CJackHwang/ds2api.git
synced 2026-05-01 23:15:27 +08:00
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:
@@ -1,3 +1,3 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = require('./chat-stream/index.js');
|
||||
module.exports = require('../internal/js/chat-stream/index.js');
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
@@ -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"},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"},
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
@@ -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|\
|
||||
|
||||
@@ -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 "$@"
|
||||
|
||||
Reference in New Issue
Block a user