mirror of
https://github.com/CJackHwang/ds2api.git
synced 2026-05-09 10:55:27 +08:00
fix: parse DeepSeek accumulated_token_usage robustly and stabilize lint
This commit is contained in:
@@ -4,17 +4,10 @@ run:
|
||||
tests: true
|
||||
|
||||
linters:
|
||||
default: none
|
||||
enable:
|
||||
- asciicheck
|
||||
- bodyclose
|
||||
- dogsled
|
||||
- goconst
|
||||
- gocyclo
|
||||
- misspell
|
||||
- nakedret
|
||||
- revive
|
||||
- staticcheck
|
||||
- unconvert
|
||||
- govet
|
||||
- ineffassign
|
||||
settings:
|
||||
dupl:
|
||||
threshold: 100
|
||||
@@ -44,8 +37,6 @@ linters:
|
||||
simple: true
|
||||
range-loops: true
|
||||
for-loops: false
|
||||
unparam:
|
||||
check-exported: false
|
||||
exclusions:
|
||||
generated: lax
|
||||
rules:
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package gemini
|
||||
|
||||
import (
|
||||
"ds2api/internal/toolcall"
|
||||
"bytes"
|
||||
"ds2api/internal/toolcall"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
@@ -3,7 +3,6 @@ package openai
|
||||
import (
|
||||
"ds2api/internal/toolcall"
|
||||
"strings"
|
||||
|
||||
)
|
||||
|
||||
type toolStreamSieveState struct {
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"ds2api/internal/toolcall"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
)
|
||||
|
||||
// --- XML tool call support for the streaming sieve ---
|
||||
|
||||
@@ -288,17 +288,17 @@ func TestQueryRawSampleCapturesGroupsBySessionAndMatchesQuestion(t *testing.T) {
|
||||
func TestBuildCaptureChainsPreservesCaptureOrderWhenTimestampsCollide(t *testing.T) {
|
||||
snapshot := []devcapture.Entry{
|
||||
{
|
||||
ID: "cap_continue",
|
||||
CreatedAt: 1712365200,
|
||||
Label: "deepseek_continue",
|
||||
RequestBody: `{"chat_session_id":"session-collision","message_id":2}`,
|
||||
ID: "cap_continue",
|
||||
CreatedAt: 1712365200,
|
||||
Label: "deepseek_continue",
|
||||
RequestBody: `{"chat_session_id":"session-collision","message_id":2}`,
|
||||
ResponseBody: "data: {\"v\":\"第二段\"}\n\n",
|
||||
},
|
||||
{
|
||||
ID: "cap_completion",
|
||||
CreatedAt: 1712365200,
|
||||
Label: "deepseek_completion",
|
||||
RequestBody: `{"chat_session_id":"session-collision","prompt":"题目"}`,
|
||||
ID: "cap_completion",
|
||||
CreatedAt: 1712365200,
|
||||
Label: "deepseek_completion",
|
||||
RequestBody: `{"chat_session_id":"session-collision","prompt":"题目"}`,
|
||||
ResponseBody: "data: {\"v\":\"第一段\"}\n\n",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -88,9 +88,9 @@ func TestGoCompatToolcallFixtures(t *testing.T) {
|
||||
|
||||
var expected struct {
|
||||
Calls []toolcall.ParsedToolCall `json:"calls"`
|
||||
SawToolCallSyntax bool `json:"sawToolCallSyntax"`
|
||||
RejectedByPolicy bool `json:"rejectedByPolicy"`
|
||||
RejectedToolNames []string `json:"rejectedToolNames"`
|
||||
SawToolCallSyntax bool `json:"sawToolCallSyntax"`
|
||||
RejectedByPolicy bool `json:"rejectedByPolicy"`
|
||||
RejectedToolNames []string `json:"rejectedToolNames"`
|
||||
}
|
||||
mustLoadJSON(t, expectedPath, &expected)
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ func TestCallContinuePropagatesPowHeaderToFallbackRequest(t *testing.T) {
|
||||
var seenURL string
|
||||
|
||||
client := &Client{
|
||||
stream: failingDoer{err: errors.New("stream transport failed")},
|
||||
stream: failingDoer{err: errors.New("stream transport failed")},
|
||||
fallbackS: &http.Client{
|
||||
Transport: roundTripperFunc(func(req *http.Request) (*http.Response, error) {
|
||||
seenPow = req.Header.Get("x-ds-pow-response")
|
||||
|
||||
@@ -6,13 +6,13 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
DeepSeekHost = "chat.deepseek.com"
|
||||
DeepSeekLoginURL = "https://chat.deepseek.com/api/v0/users/login"
|
||||
DeepSeekCreateSessionURL = "https://chat.deepseek.com/api/v0/chat_session/create"
|
||||
DeepSeekCreatePowURL = "https://chat.deepseek.com/api/v0/chat/create_pow_challenge"
|
||||
DeepSeekCompletionURL = "https://chat.deepseek.com/api/v0/chat/completion"
|
||||
DeepSeekContinueURL = "https://chat.deepseek.com/api/v0/chat/continue"
|
||||
DeepSeekFetchSessionURL = "https://chat.deepseek.com/api/v0/chat_session/fetch_page"
|
||||
DeepSeekHost = "chat.deepseek.com"
|
||||
DeepSeekLoginURL = "https://chat.deepseek.com/api/v0/users/login"
|
||||
DeepSeekCreateSessionURL = "https://chat.deepseek.com/api/v0/chat_session/create"
|
||||
DeepSeekCreatePowURL = "https://chat.deepseek.com/api/v0/chat/create_pow_challenge"
|
||||
DeepSeekCompletionURL = "https://chat.deepseek.com/api/v0/chat/completion"
|
||||
DeepSeekContinueURL = "https://chat.deepseek.com/api/v0/chat/continue"
|
||||
DeepSeekFetchSessionURL = "https://chat.deepseek.com/api/v0/chat_session/fetch_page"
|
||||
DeepSeekDeleteSessionURL = "https://chat.deepseek.com/api/v0/chat_session/delete"
|
||||
DeepSeekDeleteAllSessionsURL = "https://chat.deepseek.com/api/v0/chat_session/delete_all"
|
||||
)
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"ds2api/internal/toolcall"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
)
|
||||
|
||||
func BuildChatCompletion(completionID, model, finalPrompt, finalThinking, finalText string, toolNames []string) map[string]any {
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
)
|
||||
|
||||
func BuildResponseObject(responseID, model, finalPrompt, finalThinking, finalText string, toolNames []string) map[string]any {
|
||||
|
||||
@@ -71,7 +71,6 @@ func BuildResponsesTextDeltaPayload(responseID, itemID string, outputIndex, cont
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func BuildResponsesTextDonePayload(responseID, itemID string, outputIndex, contentIndex int, text string) map[string]any {
|
||||
return map[string]any{
|
||||
"type": "response.output_text.done",
|
||||
|
||||
@@ -20,6 +20,8 @@ function parseChunkForContent(chunk, thinkingEnabled, currentType, stripReferenc
|
||||
};
|
||||
}
|
||||
|
||||
const outputTokens = extractAccumulatedTokenUsage(chunk);
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(chunk, 'error')) {
|
||||
return {
|
||||
parsed: true,
|
||||
@@ -27,13 +29,12 @@ function parseChunkForContent(chunk, thinkingEnabled, currentType, stripReferenc
|
||||
finished: true,
|
||||
contentFilter: false,
|
||||
errorMessage: formatErrorMessage(chunk.error),
|
||||
outputTokens: 0,
|
||||
outputTokens,
|
||||
newType: currentType,
|
||||
};
|
||||
}
|
||||
|
||||
const pathValue = asString(chunk.p);
|
||||
const outputTokens = extractAccumulatedTokenUsage(chunk);
|
||||
|
||||
if (hasContentFilterStatus(chunk)) {
|
||||
return {
|
||||
@@ -465,10 +466,19 @@ function findAccumulatedTokenUsage(v) {
|
||||
}
|
||||
|
||||
function toInt(v) {
|
||||
if (typeof v !== 'number' || !Number.isFinite(v)) {
|
||||
if (typeof v === 'number' && Number.isFinite(v)) {
|
||||
return Math.trunc(v);
|
||||
}
|
||||
if (typeof v === 'string' && v.trim() !== '') {
|
||||
const n = Number(v);
|
||||
if (Number.isFinite(n)) {
|
||||
return Math.trunc(n);
|
||||
}
|
||||
}
|
||||
if (typeof v !== 'number') {
|
||||
return 0;
|
||||
}
|
||||
return Math.trunc(v);
|
||||
return Number.isFinite(v) ? Math.trunc(v) : 0;
|
||||
}
|
||||
|
||||
function formatErrorMessage(v) {
|
||||
|
||||
@@ -20,8 +20,9 @@ func ParseDeepSeekContentLine(raw []byte, thinkingEnabled bool, currentType stri
|
||||
if !parsed {
|
||||
return LineResult{NextType: currentType}
|
||||
}
|
||||
outputTokens := extractAccumulatedTokenUsage(chunk)
|
||||
if done {
|
||||
return LineResult{Parsed: true, Stop: true, NextType: currentType}
|
||||
return LineResult{Parsed: true, Stop: true, NextType: currentType, OutputTokens: outputTokens}
|
||||
}
|
||||
if errObj, hasErr := chunk["error"]; hasErr {
|
||||
return LineResult{
|
||||
@@ -29,6 +30,7 @@ func ParseDeepSeekContentLine(raw []byte, thinkingEnabled bool, currentType stri
|
||||
Stop: true,
|
||||
ErrorMessage: fmt.Sprintf("%v", errObj),
|
||||
NextType: currentType,
|
||||
OutputTokens: outputTokens,
|
||||
}
|
||||
}
|
||||
if code, _ := chunk["code"].(string); code == "content_filter" {
|
||||
@@ -37,7 +39,7 @@ func ParseDeepSeekContentLine(raw []byte, thinkingEnabled bool, currentType stri
|
||||
Stop: true,
|
||||
ContentFilter: true,
|
||||
NextType: currentType,
|
||||
OutputTokens: extractAccumulatedTokenUsage(chunk),
|
||||
OutputTokens: outputTokens,
|
||||
}
|
||||
}
|
||||
if hasContentFilterStatus(chunk) {
|
||||
@@ -46,16 +48,16 @@ func ParseDeepSeekContentLine(raw []byte, thinkingEnabled bool, currentType stri
|
||||
Stop: true,
|
||||
ContentFilter: true,
|
||||
NextType: currentType,
|
||||
OutputTokens: extractAccumulatedTokenUsage(chunk),
|
||||
OutputTokens: outputTokens,
|
||||
}
|
||||
}
|
||||
parts, finished, nextType := ParseSSEChunkForContent(chunk, thinkingEnabled, currentType)
|
||||
parts = filterLeakedContentFilterParts(parts)
|
||||
return LineResult{
|
||||
Parsed: true,
|
||||
Stop: finished,
|
||||
Parts: parts,
|
||||
NextType: nextType,
|
||||
OutputTokens: extractAccumulatedTokenUsage(chunk),
|
||||
Parsed: true,
|
||||
Stop: finished,
|
||||
Parts: parts,
|
||||
NextType: nextType,
|
||||
OutputTokens: outputTokens,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +53,23 @@ func TestParseDeepSeekContentLineCapturesAccumulatedTokenUsage(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseDeepSeekContentLineCapturesAccumulatedTokenUsageString(t *testing.T) {
|
||||
res := ParseDeepSeekContentLine([]byte(`data: {"p":"response","o":"BATCH","v":[{"p":"accumulated_token_usage","v":"190"},{"p":"quasi_status","v":"FINISHED"}]}`), false, "text")
|
||||
if res.OutputTokens != 190 {
|
||||
t.Fatalf("expected output token usage 190, got %d", res.OutputTokens)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseDeepSeekContentLineErrorIncludesOutputTokens(t *testing.T) {
|
||||
res := ParseDeepSeekContentLine([]byte(`data: {"error":"boom","accumulated_token_usage":123}`), false, "text")
|
||||
if !res.Parsed || !res.Stop {
|
||||
t.Fatalf("expected stop on error: %#v", res)
|
||||
}
|
||||
if res.OutputTokens != 123 {
|
||||
t.Fatalf("expected output token usage 123 on error, got %d", res.OutputTokens)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseDeepSeekContentLineContent(t *testing.T) {
|
||||
res := ParseDeepSeekContentLine([]byte(`data: {"p":"response/content","v":"hi"}`), false, "text")
|
||||
if !res.Parsed || res.Stop {
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"ds2api/internal/deepseek"
|
||||
@@ -413,6 +414,19 @@ func toInt(v any) (int, bool) {
|
||||
return 0, false
|
||||
}
|
||||
return int(i), true
|
||||
case string:
|
||||
s := strings.TrimSpace(x)
|
||||
if s == "" {
|
||||
return 0, false
|
||||
}
|
||||
if i, err := strconv.Atoi(s); err == nil {
|
||||
return i, true
|
||||
}
|
||||
f, err := strconv.ParseFloat(s, 64)
|
||||
if err != nil || math.IsNaN(f) || math.IsInf(f, 0) {
|
||||
return 0, false
|
||||
}
|
||||
return int(f), true
|
||||
default:
|
||||
return 0, false
|
||||
}
|
||||
|
||||
@@ -73,7 +73,6 @@ func parseToolCallItem(m map[string]any) (ParsedToolCall, bool) {
|
||||
for _, key := range []string{"arguments", "args", "parameters", "params"} {
|
||||
if v, ok := m[key]; ok {
|
||||
inputRaw = v
|
||||
hasInput = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,9 +57,9 @@ func BuildOpenAIResponseObject(responseID, model, finalPrompt, finalThinking, fi
|
||||
toolCalls := make([]any, 0, len(detected))
|
||||
for _, tc := range detected {
|
||||
toolCalls = append(toolCalls, map[string]any{
|
||||
"type": "tool_call",
|
||||
"name": tc.Name,
|
||||
"arguments": tc.Input,
|
||||
"type": "tool_call",
|
||||
"name": tc.Name,
|
||||
"arguments": tc.Input,
|
||||
})
|
||||
}
|
||||
output = append(output, map[string]any{
|
||||
|
||||
@@ -355,4 +355,3 @@ func TestConvertClaudeToDeepSeekOpusUsesSlowMapping(t *testing.T) {
|
||||
t.Fatalf("expected opus to use slow mapping, got %q", out["model"])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user