From f67cbfad351b27107a69c60eff91750b6cc9abf7 Mon Sep 17 00:00:00 2001 From: "CJACK." Date: Sun, 22 Mar 2026 09:25:01 +0800 Subject: [PATCH 1/6] fix: stop instructing fenced JSON for tool calls --- internal/adapter/openai/handler_toolcall_format.go | 8 ++++---- internal/adapter/openai/prompt_build_test.go | 6 ++++++ 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/internal/adapter/openai/handler_toolcall_format.go b/internal/adapter/openai/handler_toolcall_format.go index 6ddced4..4c38454 100644 --- a/internal/adapter/openai/handler_toolcall_format.go +++ b/internal/adapter/openai/handler_toolcall_format.go @@ -53,13 +53,13 @@ func injectToolPrompt(messages []map[string]any, tools []any, policy util.ToolCh if len(toolSchemas) == 0 { return messages, names } - toolPrompt := "You have access to these tools:\n\n" + strings.Join(toolSchemas, "\n\n") + "\n\nWhen you need to use tools, output ONLY a JSON code block like this:\n```json\n{\"tool_calls\": [{\"name\": \"tool_name\", \"input\": {\"param\": \"value\"}}]}\n```\n\n【EXAMPLE】\nUser: Please check the weather in Beijing and Shanghai, and update my todo list.\nAssistant:\n```json\n{\"tool_calls\": [\n {\"name\": \"get_weather\", \"input\": {\"city\": \"Beijing\"}},\n {\"name\": \"get_weather\", \"input\": {\"city\": \"Shanghai\"}},\n {\"name\": \"update_todo\", \"input\": {\"todos\": [{\"content\": \"Buy milk\"}, {\"content\": \"Write report\"}]}}\n]}\n```\n\nIMPORTANT:\n1) If calling tools, output ONLY the JSON code block. The response must start with ```json and end with ```.\n2) After receiving a tool result, you MUST use it to produce the final answer.\n3) Only call another tool when the previous result is missing required data or returned an error.\n4) JSON SYNTAX STRICTLY REQUIRED: All property names MUST be enclosed in double quotes (e.g., \"name\", not name).\n5) ARRAY FORMAT: If providing a list of items, you MUST enclose them in square brackets `[]` (e.g., \"todos\": [{\"item\": \"a\"}, {\"item\": \"b\"}]). DO NOT output comma-separated objects without brackets." + toolPrompt := "You have access to these tools:\n\n" + strings.Join(toolSchemas, "\n\n") + "\n\nWhen you need to use tools, output ONLY this JSON object format:\n{\"tool_calls\": [{\"name\": \"tool_name\", \"input\": {\"param\": \"value\"}}]}\n\n【EXAMPLE】\nUser: Please check the weather in Beijing and Shanghai, and update my todo list.\nAssistant:\n{\"tool_calls\": [\n {\"name\": \"get_weather\", \"input\": {\"city\": \"Beijing\"}},\n {\"name\": \"get_weather\", \"input\": {\"city\": \"Shanghai\"}},\n {\"name\": \"update_todo\", \"input\": {\"todos\": [{\"content\": \"Buy milk\"}, {\"content\": \"Write report\"}]}}\n]}\n\nIMPORTANT:\n1) If calling tools, output ONLY the JSON object above. Do NOT include any extra text.\n2) Do NOT wrap tool-call JSON in markdown/code fences (for example, do not use triple backticks).\n3) After receiving a tool result, you MUST use it to produce the final answer.\n4) Only call another tool when the previous result is missing required data or returned an error.\n5) JSON SYNTAX STRICTLY REQUIRED: All property names MUST be enclosed in double quotes (e.g., \"name\", not name).\n6) ARRAY FORMAT: If providing a list of items, you MUST enclose them in square brackets `[]` (e.g., \"todos\": [{\"item\": \"a\"}, {\"item\": \"b\"}]). DO NOT output comma-separated objects without brackets." if policy.Mode == util.ToolChoiceRequired { - toolPrompt += "\n5) For this response, you MUST call at least one tool from the allowed list." + toolPrompt += "\n7) For this response, you MUST call at least one tool from the allowed list." } if policy.Mode == util.ToolChoiceForced && strings.TrimSpace(policy.ForcedName) != "" { - toolPrompt += "\n5) For this response, you MUST call exactly this tool name: " + strings.TrimSpace(policy.ForcedName) - toolPrompt += "\n6) Do not call any other tool." + toolPrompt += "\n7) For this response, you MUST call exactly this tool name: " + strings.TrimSpace(policy.ForcedName) + toolPrompt += "\n8) Do not call any other tool." } for i := range messages { diff --git a/internal/adapter/openai/prompt_build_test.go b/internal/adapter/openai/prompt_build_test.go index c7d4dc2..ed48740 100644 --- a/internal/adapter/openai/prompt_build_test.go +++ b/internal/adapter/openai/prompt_build_test.go @@ -77,4 +77,10 @@ func TestBuildOpenAIFinalPrompt_VercelPreparePathKeepsFinalAnswerInstruction(t * if !strings.Contains(finalPrompt, "Only call another tool when the previous result is missing required data or returned an error.") { t.Fatalf("vercel prepare finalPrompt missing retry guard instruction: %q", finalPrompt) } + if !strings.Contains(finalPrompt, "Do NOT wrap tool-call JSON in markdown/code fences") { + t.Fatalf("vercel prepare finalPrompt missing no-fence instruction: %q", finalPrompt) + } + if strings.Contains(finalPrompt, "```json") { + t.Fatalf("vercel prepare finalPrompt should not require fenced json tool calls: %q", finalPrompt) + } } From fe0f3d2c17a3ffdf6ce8845f142730e749b3b3ea Mon Sep 17 00:00:00 2001 From: "CJACK." Date: Sun, 22 Mar 2026 09:29:21 +0800 Subject: [PATCH 2/6] fix: strip empty json fences from sanitized stream text --- internal/adapter/openai/tool_history_sanitize.go | 5 ++++- internal/adapter/openai/tool_history_sanitize_test.go | 8 ++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/internal/adapter/openai/tool_history_sanitize.go b/internal/adapter/openai/tool_history_sanitize.go index 6a2e80a..126414a 100644 --- a/internal/adapter/openai/tool_history_sanitize.go +++ b/internal/adapter/openai/tool_history_sanitize.go @@ -5,10 +5,13 @@ import ( ) var leakedToolHistoryPattern = regexp.MustCompile(`(?is)\[TOOL_CALL_HISTORY\][\s\S]*?\[/TOOL_CALL_HISTORY\]|\[TOOL_RESULT_HISTORY\][\s\S]*?\[/TOOL_RESULT_HISTORY\]`) +var emptyJSONFencePattern = regexp.MustCompile("(?is)```json\\s*```") func sanitizeLeakedToolHistory(text string) string { if text == "" { return text } - return leakedToolHistoryPattern.ReplaceAllString(text, "") + out := leakedToolHistoryPattern.ReplaceAllString(text, "") + out = emptyJSONFencePattern.ReplaceAllString(out, "") + return out } diff --git a/internal/adapter/openai/tool_history_sanitize_test.go b/internal/adapter/openai/tool_history_sanitize_test.go index 02128c9..7c10ad2 100644 --- a/internal/adapter/openai/tool_history_sanitize_test.go +++ b/internal/adapter/openai/tool_history_sanitize_test.go @@ -43,6 +43,14 @@ func TestSanitizeLeakedToolHistoryPreservesChunkWhitespace(t *testing.T) { } } +func TestSanitizeLeakedToolHistoryRemovesEmptyJSONFence(t *testing.T) { + raw := "before\n```json\n```\nafter" + got := sanitizeLeakedToolHistory(raw) + if got != "before\n\nafter" { + t.Fatalf("unexpected sanitized empty json fence: %q", got) + } +} + func TestFlushToolSieveDropsToolHistoryLeak(t *testing.T) { var state toolStreamSieveState chunk := "[TOOL_CALL_HISTORY]\nstatus: already_called\nfunction.name: exec\nfunction.arguments: {}\n[/TOOL_CALL_HISTORY]" From 00d38f118702ffed506ee0cc2d14f661255d2cbc Mon Sep 17 00:00:00 2001 From: "CJACK." Date: Sun, 22 Mar 2026 09:58:29 +0800 Subject: [PATCH 3/6] fix: parse claude tool_use function/parameter format --- internal/util/toolcalls_parse_markup.go | 28 +++++++++++++++++++++++++ internal/util/toolcalls_test.go | 14 +++++++++++++ 2 files changed, 42 insertions(+) diff --git a/internal/util/toolcalls_parse_markup.go b/internal/util/toolcalls_parse_markup.go index e2eff83..85e7138 100644 --- a/internal/util/toolcalls_parse_markup.go +++ b/internal/util/toolcalls_parse_markup.go @@ -15,6 +15,7 @@ var antmlArgumentPattern = regexp.MustCompile(`(?is)<(?:[a-z0-9_]+:)?argument\s+ var antmlParametersPattern = regexp.MustCompile(`(?is)<(?:[a-z0-9_]+:)?parameters\s*>\s*(\{.*?\})\s*`) var invokeCallPattern = regexp.MustCompile(`(?is)(.*?)`) var invokeParamPattern = regexp.MustCompile(`(?is)\s*(.*?)\s*`) +var toolUseFunctionPattern = regexp.MustCompile(`(?is)\s*(.*?)\s*`) func parseXMLToolCalls(text string) []ParsedToolCall { matches := xmlToolCallPattern.FindAllString(text, -1) @@ -38,6 +39,9 @@ func parseXMLToolCalls(text string) []ParsedToolCall { if call, ok := parseInvokeFunctionCallStyle(text); ok { return []ParsedToolCall{call} } + if call, ok := parseToolUseFunctionStyle(text); ok { + return []ParsedToolCall{call} + } return nil } @@ -229,6 +233,30 @@ func parseInvokeFunctionCallStyle(text string) (ParsedToolCall, bool) { return ParsedToolCall{Name: name, Input: input}, true } +func parseToolUseFunctionStyle(text string) (ParsedToolCall, bool) { + m := toolUseFunctionPattern.FindStringSubmatch(text) + if len(m) < 3 { + return ParsedToolCall{}, false + } + name := strings.TrimSpace(m[1]) + if name == "" { + return ParsedToolCall{}, false + } + body := m[2] + input := map[string]any{} + for _, pm := range invokeParamPattern.FindAllStringSubmatch(body, -1) { + if len(pm) < 3 { + continue + } + k := strings.TrimSpace(pm[1]) + v := strings.TrimSpace(pm[2]) + if k != "" { + input[k] = v + } + } + return ParsedToolCall{Name: name, Input: input}, true +} + func asString(v any) string { s, _ := v.(string) return s diff --git a/internal/util/toolcalls_test.go b/internal/util/toolcalls_test.go index 9701a46..d66519b 100644 --- a/internal/util/toolcalls_test.go +++ b/internal/util/toolcalls_test.go @@ -236,6 +236,20 @@ func TestParseToolCallsSupportsInvokeFunctionCallStyle(t *testing.T) { } } +func TestParseToolCallsSupportsToolUseFunctionParameterStyle(t *testing.T) { + text := `test` + calls := ParseToolCalls(text, []string{"search_web"}) + if len(calls) != 1 { + t.Fatalf("expected 1 call, got %#v", calls) + } + if calls[0].Name != "search_web" { + t.Fatalf("expected canonical tool name search_web, got %q", calls[0].Name) + } + if calls[0].Input["query"] != "test" { + t.Fatalf("expected query argument, got %#v", calls[0].Input) + } +} + func TestParseToolCallsSupportsNestedToolTagStyle(t *testing.T) { text := `pwdshow cwd` calls := ParseToolCalls(text, []string{"bash"}) From 3fccec0e22241c153b9d9e87804c474c02dc3594 Mon Sep 17 00:00:00 2001 From: "CJACK." Date: Sun, 22 Mar 2026 10:24:11 +0800 Subject: [PATCH 4/6] test: remove unused asFloat helper --- .../adapter/openai/handler_toolcall_policy.go | 6 - .../openai/responses_stream_runtime_core.go | 10 - .../adapter/openai/responses_stream_test.go | 15 - .../adapter/openai/tool_sieve_incremental.go | 288 ------------------ .../adapter/openai/tool_sieve_jsonscan.go | 109 ------- internal/adapter/openai/tool_sieve_state.go | 11 - internal/deepseek/client_http_json.go | 11 - internal/util/toolcalls_candidates.go | 14 +- webui/src/components/AccountManager.jsx | 3 - webui/src/components/ApiTester.jsx | 3 - webui/src/components/Settings.jsx | 3 - webui/src/components/VercelSync.jsx | 3 - webui/src/layout/DashboardShell.jsx | 16 +- 13 files changed, 11 insertions(+), 481 deletions(-) delete mode 100644 internal/adapter/openai/tool_sieve_incremental.go delete mode 100644 webui/src/components/AccountManager.jsx delete mode 100644 webui/src/components/ApiTester.jsx delete mode 100644 webui/src/components/Settings.jsx delete mode 100644 webui/src/components/VercelSync.jsx diff --git a/internal/adapter/openai/handler_toolcall_policy.go b/internal/adapter/openai/handler_toolcall_policy.go index 9f0e839..7b6f39c 100644 --- a/internal/adapter/openai/handler_toolcall_policy.go +++ b/internal/adapter/openai/handler_toolcall_policy.go @@ -2,12 +2,6 @@ package openai import "strings" -func applyOpenAIChatPassThrough(req map[string]any, payload map[string]any) { - for k, v := range collectOpenAIChatPassThrough(req) { - payload[k] = v - } -} - func (h *Handler) toolcallFeatureMatchEnabled() bool { if h == nil || h.Store == nil { return true diff --git a/internal/adapter/openai/responses_stream_runtime_core.go b/internal/adapter/openai/responses_stream_runtime_core.go index c1ca926..727bae0 100644 --- a/internal/adapter/openai/responses_stream_runtime_core.go +++ b/internal/adapter/openai/responses_stream_runtime_core.go @@ -32,7 +32,6 @@ type responsesStreamRuntime struct { toolCallsDoneEmitted bool sieve toolStreamSieveState - thinkingSieve toolStreamSieveState thinking strings.Builder text strings.Builder visibleText strings.Builder @@ -169,15 +168,6 @@ func (s *responsesStreamRuntime) logToolPolicyRejections(textParsed util.ToolCal logRejected(textParsed, "text") } -func (s *responsesStreamRuntime) hasFunctionCallDone() bool { - for _, done := range s.functionDone { - if done { - return true - } - } - return false -} - func (s *responsesStreamRuntime) onParsed(parsed sse.LineResult) streamengine.ParsedDecision { if !parsed.Parsed { return streamengine.ParsedDecision{} diff --git a/internal/adapter/openai/responses_stream_test.go b/internal/adapter/openai/responses_stream_test.go index 7d15ede..02d1f4b 100644 --- a/internal/adapter/openai/responses_stream_test.go +++ b/internal/adapter/openai/responses_stream_test.go @@ -675,18 +675,3 @@ func extractAllSSEEventPayloads(body, targetEvent string) []map[string]any { } return out } - -func asFloat(v any) float64 { - switch x := v.(type) { - case float64: - return x - case float32: - return float64(x) - case int: - return float64(x) - case int64: - return float64(x) - default: - return 0 - } -} diff --git a/internal/adapter/openai/tool_sieve_incremental.go b/internal/adapter/openai/tool_sieve_incremental.go deleted file mode 100644 index d0d7842..0000000 --- a/internal/adapter/openai/tool_sieve_incremental.go +++ /dev/null @@ -1,288 +0,0 @@ -package openai - -import "strings" - -func buildIncrementalToolDeltas(state *toolStreamSieveState) []toolCallDelta { - if state.disableDeltas { - return nil - } - captured := state.capture.String() - if captured == "" { - return nil - } - lower := strings.ToLower(captured) - keyIdx := strings.Index(lower, "tool_calls") - if keyIdx < 0 { - return nil - } - start := strings.LastIndex(captured[:keyIdx], "{") - if start < 0 { - return nil - } - certainSingle, hasMultiple := classifyToolCallsIncrementalSafety(captured, keyIdx) - if hasMultiple { - state.disableDeltas = true - return nil - } - if !certainSingle { - // In uncertain phases (e.g. first call arrived but array not closed yet), - // avoid speculative deltas and wait for final parsed tool_calls payload. - return nil - } - callStart, ok := findFirstToolCallObjectStart(captured, keyIdx) - if !ok { - return nil - } - deltas := make([]toolCallDelta, 0, 2) - if state.toolName == "" { - name, ok := extractToolCallName(captured, callStart) - if !ok || name == "" { - return nil - } - state.toolName = name - } - if state.toolArgsStart < 0 { - argsStart, stringMode, ok := findToolCallArgsStart(captured, callStart) - if ok { - state.toolArgsString = stringMode - if stringMode { - state.toolArgsStart = argsStart + 1 - } else { - state.toolArgsStart = argsStart - } - state.toolArgsSent = state.toolArgsStart - } - } - if !state.toolNameSent { - if state.toolArgsStart < 0 { - return nil - } - state.toolNameSent = true - deltas = append(deltas, toolCallDelta{Index: 0, Name: state.toolName}) - } - if state.toolArgsStart < 0 || state.toolArgsDone { - return deltas - } - end, complete, ok := scanToolCallArgsProgress(captured, state.toolArgsStart, state.toolArgsString) - if !ok { - return deltas - } - if end > state.toolArgsSent { - deltas = append(deltas, toolCallDelta{ - Index: 0, - Arguments: captured[state.toolArgsSent:end], - }) - state.toolArgsSent = end - } - if complete { - state.toolArgsDone = true - } - return deltas -} - -func classifyToolCallsIncrementalSafety(text string, keyIdx int) (certainSingle bool, hasMultiple bool) { - arrStart, ok := findToolCallsArrayStart(text, keyIdx) - if !ok { - return false, false - } - i := skipSpaces(text, arrStart+1) - if i >= len(text) || text[i] != '{' { - return false, false - } - count := 0 - depth := 0 - quote := byte(0) - escaped := false - for ; i < len(text); i++ { - ch := text[i] - if quote != 0 { - if escaped { - escaped = false - continue - } - if ch == '\\' { - escaped = true - continue - } - if ch == quote { - quote = 0 - } - continue - } - if ch == '"' || ch == '\'' { - quote = ch - continue - } - if ch == '{' { - if depth == 0 { - count++ - if count > 1 { - return false, true - } - } - depth++ - continue - } - if ch == '}' { - if depth > 0 { - depth-- - } - continue - } - if ch == ',' && depth == 0 { - // top-level separator means at least one more tool call exists - // (or is expected). Treat as multi-call and stop incremental deltas. - return false, true - } - if ch == ']' && depth == 0 { - return count == 1, false - } - } - // array not closed yet: still uncertain whether more calls will appear - return false, false -} - -func findFirstToolCallObjectStart(text string, keyIdx int) (int, bool) { - arrStart, ok := findToolCallsArrayStart(text, keyIdx) - if !ok { - return -1, false - } - i := skipSpaces(text, arrStart+1) - if i >= len(text) || text[i] != '{' { - return -1, false - } - return i, true -} - -func findToolCallsArrayStart(text string, keyIdx int) (int, bool) { - i := keyIdx + len("tool_calls") - for i < len(text) && text[i] != ':' { - i++ - } - if i >= len(text) { - return -1, false - } - i = skipSpaces(text, i+1) - if i >= len(text) || text[i] != '[' { - return -1, false - } - return i, true -} - -func extractToolCallName(text string, callStart int) (string, bool) { - valueStart, ok := findObjectFieldValueStart(text, callStart, []string{"name"}) - if !ok || valueStart >= len(text) || text[valueStart] != '"' { - fnStart, fnOK := findFunctionObjectStart(text, callStart) - if !fnOK { - return "", false - } - valueStart, ok = findObjectFieldValueStart(text, fnStart, []string{"name"}) - if !ok || valueStart >= len(text) || text[valueStart] != '"' { - return "", false - } - } - name, _, ok := parseJSONStringLiteral(text, valueStart) - if !ok { - return "", false - } - return name, true -} - -func findToolCallArgsStart(text string, callStart int) (int, bool, bool) { - keys := []string{"input", "arguments", "args", "parameters", "params"} - valueStart, ok := findObjectFieldValueStart(text, callStart, keys) - if !ok { - fnStart, fnOK := findFunctionObjectStart(text, callStart) - if !fnOK { - return -1, false, false - } - valueStart, ok = findObjectFieldValueStart(text, fnStart, keys) - if !ok { - return -1, false, false - } - } - if valueStart >= len(text) { - return -1, false, false - } - ch := text[valueStart] - if ch == '{' || ch == '[' { - return valueStart, false, true - } - if ch == '"' { - return valueStart, true, true - } - return -1, false, false -} - -func scanToolCallArgsProgress(text string, start int, stringMode bool) (int, bool, bool) { - if start < 0 || start > len(text) { - return 0, false, false - } - if stringMode { - escaped := false - for i := start; i < len(text); i++ { - ch := text[i] - if escaped { - escaped = false - continue - } - if ch == '\\' { - escaped = true - continue - } - if ch == '"' { - return i, true, true - } - } - return len(text), false, true - } - if start >= len(text) { - return start, false, false - } - if text[start] != '{' && text[start] != '[' { - return 0, false, false - } - depth := 0 - quote := byte(0) - escaped := false - for i := start; i < len(text); i++ { - ch := text[i] - if quote != 0 { - if escaped { - escaped = false - continue - } - if ch == '\\' { - escaped = true - continue - } - if ch == quote { - quote = 0 - } - continue - } - if ch == '"' || ch == '\'' { - quote = ch - continue - } - if ch == '{' || ch == '[' { - depth++ - continue - } - if ch == '}' || ch == ']' { - depth-- - if depth == 0 { - return i + 1, true, true - } - } - } - return len(text), false, true -} - -func findFunctionObjectStart(text string, callStart int) (int, bool) { - valueStart, ok := findObjectFieldValueStart(text, callStart, []string{"function"}) - if !ok || valueStart >= len(text) || text[valueStart] != '{' { - return -1, false - } - return valueStart, true -} diff --git a/internal/adapter/openai/tool_sieve_jsonscan.go b/internal/adapter/openai/tool_sieve_jsonscan.go index d3abcc5..b49ef7a 100644 --- a/internal/adapter/openai/tool_sieve_jsonscan.go +++ b/internal/adapter/openai/tool_sieve_jsonscan.go @@ -1,7 +1,5 @@ package openai -import "strings" - func extractJSONObjectFrom(text string, start int) (string, int, bool) { if start < 0 || start >= len(text) || text[start] != '{' { return "", 0, false @@ -43,110 +41,3 @@ func extractJSONObjectFrom(text string, start int) (string, int, bool) { } return "", 0, false } - -func findObjectFieldValueStart(text string, objStart int, keys []string) (int, bool) { - if objStart < 0 || objStart >= len(text) || text[objStart] != '{' { - return 0, false - } - depth := 0 - quote := byte(0) - escaped := false - for i := objStart; i < len(text); i++ { - ch := text[i] - if quote != 0 { - if escaped { - escaped = false - continue - } - if ch == '\\' { - escaped = true - continue - } - if ch == quote { - quote = 0 - } - continue - } - if ch == '"' || ch == '\'' { - if depth == 1 { - key, end, ok := parseJSONStringLiteral(text, i) - if !ok { - return 0, false - } - j := skipSpaces(text, end) - if j >= len(text) || text[j] != ':' { - i = end - 1 - continue - } - j = skipSpaces(text, j+1) - if j >= len(text) { - return 0, false - } - if containsKey(keys, key) { - return j, true - } - i = j - 1 - continue - } - quote = ch - continue - } - if ch == '{' { - depth++ - continue - } - if ch == '}' { - depth-- - if depth == 0 { - break - } - } - } - return 0, false -} - -func parseJSONStringLiteral(text string, start int) (string, int, bool) { - if start < 0 || start >= len(text) || text[start] != '"' { - return "", 0, false - } - var b strings.Builder - escaped := false - for i := start + 1; i < len(text); i++ { - ch := text[i] - if escaped { - b.WriteByte(ch) - escaped = false - continue - } - if ch == '\\' { - escaped = true - continue - } - if ch == '"' { - return b.String(), i + 1, true - } - b.WriteByte(ch) - } - return "", 0, false -} - -func containsKey(keys []string, value string) bool { - for _, k := range keys { - if k == value { - return true - } - } - return false -} - -func skipSpaces(text string, i int) int { - for i < len(text) { - switch text[i] { - case ' ', '\t', '\n', '\r': - i++ - default: - return i - } - } - return i -} diff --git a/internal/adapter/openai/tool_sieve_state.go b/internal/adapter/openai/tool_sieve_state.go index 1db9413..f36560a 100644 --- a/internal/adapter/openai/tool_sieve_state.go +++ b/internal/adapter/openai/tool_sieve_state.go @@ -63,14 +63,3 @@ func appendTail(prev, next string, max int) string { } return combined[len(combined)-max:] } - -func looksLikeToolExampleContext(text string) bool { - return insideCodeFence(text) -} - -func insideCodeFence(text string) bool { - if text == "" { - return false - } - return strings.Count(text, "```")%2 == 1 -} diff --git a/internal/deepseek/client_http_json.go b/internal/deepseek/client_http_json.go index 1620b2e..a35d736 100644 --- a/internal/deepseek/client_http_json.go +++ b/internal/deepseek/client_http_json.go @@ -63,17 +63,6 @@ func (c *Client) postJSONWithStatus(ctx context.Context, doer trans.Doer, url st return out, resp.StatusCode, nil } -func (c *Client) getJSON(ctx context.Context, doer trans.Doer, url string, headers map[string]string) (map[string]any, error) { - body, status, err := c.getJSONWithStatus(ctx, doer, url, headers) - if err != nil { - return nil, err - } - if status == 0 { - return nil, errors.New("request failed") - } - return body, nil -} - func (c *Client) getJSONWithStatus(ctx context.Context, doer trans.Doer, url string, headers map[string]string) (map[string]any, int, error) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { diff --git a/internal/util/toolcalls_candidates.go b/internal/util/toolcalls_candidates.go index 49db011..e4495b7 100644 --- a/internal/util/toolcalls_candidates.go +++ b/internal/util/toolcalls_candidates.go @@ -7,7 +7,6 @@ import ( var toolCallPattern = regexp.MustCompile(`\{\s*["']tool_calls["']\s*:\s*\[(.*?)\]\s*\}`) var fencedJSONPattern = regexp.MustCompile("(?s)```(?:json)?\\s*(.*?)\\s*```") -var fencedBlockPattern = regexp.MustCompile("(?s)```.*?```") func buildToolCallCandidates(text string) []string { trimmed := strings.TrimSpace(text) @@ -82,12 +81,12 @@ func extractToolCallObjects(text string) []string { if searchLimit < offset { searchLimit = offset } - + start := strings.LastIndex(text[searchLimit:idx], "{") if start >= 0 { start += searchLimit } - + if start < 0 { offset = idx + len(matchedKeyword) continue @@ -113,7 +112,7 @@ func extractToolCallObjects(text string) []string { } break } - + if !foundObj { offset = idx + len(matchedKeyword) } @@ -174,10 +173,3 @@ func looksLikeToolExampleContext(text string) bool { } return strings.Contains(t, "```") } - -func stripFencedCodeBlocks(text string) string { - if strings.TrimSpace(text) == "" { - return "" - } - return fencedBlockPattern.ReplaceAllString(text, " ") -} diff --git a/webui/src/components/AccountManager.jsx b/webui/src/components/AccountManager.jsx deleted file mode 100644 index 2a37010..0000000 --- a/webui/src/components/AccountManager.jsx +++ /dev/null @@ -1,3 +0,0 @@ -import AccountManagerContainer from '../features/account/AccountManagerContainer' - -export default AccountManagerContainer diff --git a/webui/src/components/ApiTester.jsx b/webui/src/components/ApiTester.jsx deleted file mode 100644 index b688195..0000000 --- a/webui/src/components/ApiTester.jsx +++ /dev/null @@ -1,3 +0,0 @@ -import ApiTesterContainer from '../features/apiTester/ApiTesterContainer' - -export default ApiTesterContainer diff --git a/webui/src/components/Settings.jsx b/webui/src/components/Settings.jsx deleted file mode 100644 index 317374e..0000000 --- a/webui/src/components/Settings.jsx +++ /dev/null @@ -1,3 +0,0 @@ -import SettingsContainer from '../features/settings/SettingsContainer' - -export default SettingsContainer diff --git a/webui/src/components/VercelSync.jsx b/webui/src/components/VercelSync.jsx deleted file mode 100644 index 853b9e2..0000000 --- a/webui/src/components/VercelSync.jsx +++ /dev/null @@ -1,3 +0,0 @@ -import VercelSyncContainer from '../features/vercel/VercelSyncContainer' - -export default VercelSyncContainer diff --git a/webui/src/layout/DashboardShell.jsx b/webui/src/layout/DashboardShell.jsx index b0dc5ea..1a5d5c4 100644 --- a/webui/src/layout/DashboardShell.jsx +++ b/webui/src/layout/DashboardShell.jsx @@ -12,11 +12,11 @@ import { } from 'lucide-react' import clsx from 'clsx' -import AccountManager from '../components/AccountManager' -import ApiTester from '../components/ApiTester' +import AccountManagerContainer from '../features/account/AccountManagerContainer' +import ApiTesterContainer from '../features/apiTester/ApiTesterContainer' import BatchImport from '../components/BatchImport' -import VercelSync from '../components/VercelSync' -import Settings from '../components/Settings' +import VercelSyncContainer from '../features/vercel/VercelSyncContainer' +import SettingsContainer from '../features/settings/SettingsContainer' import LanguageToggle from '../components/LanguageToggle' import { useI18n } from '../i18n' @@ -73,15 +73,15 @@ export default function DashboardShell({ token, onLogout, config, fetchConfig, s const renderTab = () => { switch (activeTab) { case 'accounts': - return + return case 'test': - return + return case 'import': return case 'vercel': - return + return case 'settings': - return + return default: return null } From 5031ae0e6f971f5d820eceb0bdabdcbebd36f176 Mon Sep 17 00:00:00 2001 From: "CJACK." Date: Sun, 22 Mar 2026 10:38:08 +0800 Subject: [PATCH 5/6] ci: align refactor line gate with removed files --- plans/refactor-line-gate-targets.txt | 5 ----- tests/scripts/check-refactor-line-gate.sh | 6 +----- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/plans/refactor-line-gate-targets.txt b/plans/refactor-line-gate-targets.txt index ac45d57..c1ffd22 100644 --- a/plans/refactor-line-gate-targets.txt +++ b/plans/refactor-line-gate-targets.txt @@ -53,7 +53,6 @@ internal/adapter/openai/responses_stream_runtime_events.go internal/adapter/openai/responses_stream_runtime_toolcalls.go internal/adapter/openai/tool_sieve_state.go internal/adapter/openai/tool_sieve_core.go -internal/adapter/openai/tool_sieve_incremental.go internal/adapter/openai/tool_sieve_jsonscan.go internal/util/toolcalls_parse.go @@ -117,7 +116,6 @@ webui/src/app/useAdminAuth.js webui/src/app/useAdminConfig.js webui/src/layout/DashboardShell.jsx -webui/src/components/AccountManager.jsx webui/src/features/account/AccountManagerContainer.jsx webui/src/features/account/useAccountsData.js webui/src/features/account/useAccountActions.js @@ -127,14 +125,12 @@ webui/src/features/account/AccountsTable.jsx webui/src/features/account/AddKeyModal.jsx webui/src/features/account/AddAccountModal.jsx -webui/src/components/ApiTester.jsx webui/src/features/apiTester/ApiTesterContainer.jsx webui/src/features/apiTester/useApiTesterState.js webui/src/features/apiTester/useChatStreamClient.js webui/src/features/apiTester/ConfigPanel.jsx webui/src/features/apiTester/ChatPanel.jsx -webui/src/components/Settings.jsx webui/src/features/settings/SettingsContainer.jsx webui/src/features/settings/useSettingsForm.js webui/src/features/settings/settingsApi.js @@ -144,7 +140,6 @@ webui/src/features/settings/BehaviorSection.jsx webui/src/features/settings/ModelSection.jsx webui/src/features/settings/BackupSection.jsx -webui/src/components/VercelSync.jsx webui/src/features/vercel/VercelSyncContainer.jsx webui/src/features/vercel/useVercelSyncState.js webui/src/features/vercel/VercelSyncForm.jsx diff --git a/tests/scripts/check-refactor-line-gate.sh b/tests/scripts/check-refactor-line-gate.sh index 3fda714..4a48827 100755 --- a/tests/scripts/check-refactor-line-gate.sh +++ b/tests/scripts/check-refactor-line-gate.sh @@ -12,11 +12,7 @@ is_entry_file() { case "$1" in api/chat-stream.js|\ internal/js/helpers/stream-tool-sieve.js|\ - webui/src/App.jsx|\ - webui/src/components/AccountManager.jsx|\ - webui/src/components/ApiTester.jsx|\ - webui/src/components/Settings.jsx|\ - webui/src/components/VercelSync.jsx) + webui/src/App.jsx) return 0 ;; esac From 455489ffebe9906fc6880b2d6857a860217ea631 Mon Sep 17 00:00:00 2001 From: "CJACK." Date: Sun, 22 Mar 2026 10:38:18 +0800 Subject: [PATCH 6/6] ci: upgrade GitHub Actions Node runtime to 24 --- .github/workflows/quality-gates.yml | 2 +- .github/workflows/release-artifacts.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/quality-gates.yml b/.github/workflows/quality-gates.yml index 3d7c9a1..8f1d865 100644 --- a/.github/workflows/quality-gates.yml +++ b/.github/workflows/quality-gates.yml @@ -24,7 +24,7 @@ jobs: - name: Setup Node uses: actions/setup-node@v4 with: - node-version: "20" + node-version: "24" cache: "npm" cache-dependency-path: webui/package-lock.json diff --git a/.github/workflows/release-artifacts.yml b/.github/workflows/release-artifacts.yml index d2ba851..133c509 100644 --- a/.github/workflows/release-artifacts.yml +++ b/.github/workflows/release-artifacts.yml @@ -32,7 +32,7 @@ jobs: - name: Setup Node uses: actions/setup-node@v4 with: - node-version: "20" + node-version: "24" cache: "npm" cache-dependency-path: webui/package-lock.json