返回 DeepSeek-Reasonix
vendor.go
根目录 / internal / provider / responses / vendor.go
1 package responses
2
3 import (
4 "net/url"
5 "strings"
6
7 "reasonix/internal/provider"
8 )
9
10 // vendorCapabilities describes how a Responses-compatible endpoint deviates
11 // from the base OpenAI Responses wire behavior. Vendors are detected from the
12 // base URL (DetectVendor); unknown endpoints get the zero value, which is the
13 // standard OpenAI-compatible behavior (stateful, no session-cache header, no
14 // summary requirement, no tool-call reasoning retention, temperature honored).
15 //
16 // This table is the single source of truth for wire-level vendor differences:
17 // adding a new Responses-compatible vendor means adding one entry here and a
18 // base-URL case in DetectVendor — not threading more string comparisons
19 // through responses.go.
20 type vendorCapabilities struct {
21 // stateless marks endpoints that reject previous_response_id and require
22 // the full input history on every turn (DeepSeek, MiMo). stateful is the
23 // OpenAI default.
24 stateless bool
25
26 // sessionCacheHeader marks DashScope, whose session cache must be opted
27 // into with the x-dashscope-session-cache header.
28 sessionCacheHeader bool
29
30 // toolCallReasoning marks stateless vendors whose documentation requires
31 // retaining historical reasoning content in the input on multi-turn tool
32 // calls (DeepSeek, MiMo).
33 toolCallReasoning bool
34
35 // singleSegmentReasoning marks endpoints whose thinking is one
36 // uninterruptible segment per turn: the server emits reasoning and the
37 // final answer atomically, and a new reasoning segment only starts on a
38 // brand-new turn — never mid-turn after a tool call. MiMo documents this
39 // ("reasoning.effort: low/medium/high all enable reasoning, no strength
40 // differentiation"; tool-call turns carry one segment). DeepSeek, by
41 // contrast, can emit several reasoning segments across a turn's tool
42 // loop. Callers must not expect a multi-segment chain-of-thought from
43 // single-segment vendors.
44 singleSegmentReasoning bool
45
46 // ignoresTemperature marks vendors that force temperature/top_p to their
47 // defaults in thinking mode, so sending them is a no-op (MiMo forces
48 // 1.0 / 0.95). Keeps the wire request lean for such endpoints.
49 ignoresTemperature bool
50
51 // defaultMaxOutputTokens is the max_output_tokens sent when the caller
52 // did not request one (req.MaxTokens == 0). Zero means "leave unset and
53 // let the server use its own default". MiMo's server default (32768)
54 // covers reasoning + visible output, and its thinking mode can spend a
55 // large chunk of that budget on reasoning before the visible answer —
56 // truncating tool calls mid-JSON on long turns. Raise it to the next
57 // documented tier (65536, within the allowed [1, 131072] range) so the
58 // answer survives long reasoning.
59 defaultMaxOutputTokens int
60
61 // compactionOutputTokens is the separate budget for native/summary
62 // compaction calls. Zero means "no dedicated compaction budget; fall
63 // back to ordinary summarize without inheriting a large default".
64 compactionOutputTokens int
65
66 // summaryRequired marks vendors whose Responses API requires the
67 // `summary` list on input reasoning items (DashScope; without it the
68 // server rejects with "Invalid 'summary': summary is required..."). The
69 // OpenAI base format only needs `content`. Sending `summary` to vendors
70 // that do not define it (MiMo) leaks the reasoning text into an extra
71 // field the server may fold back into the model context, doubling the
72 // chain-of-thought echoed each turn and inflating reasoning output
73 // until truncation. Only send it where the wire demands it.
74 summaryRequired bool
75 }
76
77 var vendorTable = map[string]vendorCapabilities{
78 "dashscope": {
79 stateless: false,
80 sessionCacheHeader: true,
81 toolCallReasoning: false,
82 singleSegmentReasoning: false,
83 ignoresTemperature: false,
84 summaryRequired: true,
85 // No native compact endpoint yet; summarize fallback only.
86 compactionOutputTokens: 8192,
87 },
88 "deepseek": {
89 stateless: true,
90 sessionCacheHeader: false,
91 toolCallReasoning: true,
92 singleSegmentReasoning: false,
93 ignoresTemperature: false,
94 // 0 = omit max_output_tokens; official server ceiling is 384K.
95 defaultMaxOutputTokens: 0,
96 // Compaction summaries use a dedicated 16K-class budget, independent of
97 // ordinary answer output.
98 compactionOutputTokens: provider.DefaultOrdinaryOutputTokens,
99 },
100 "mimo": {
101 stateless: true,
102 sessionCacheHeader: false,
103 toolCallReasoning: true,
104 singleSegmentReasoning: true,
105 ignoresTemperature: true,
106 // Coding-agent default 32K; users may raise explicitly. Not 128K auto.
107 defaultMaxOutputTokens: provider.DefaultReasoningOutputTokens,
108 compactionOutputTokens: provider.DefaultOrdinaryOutputTokens,
109 },
110 // StepFun's Responses API accepts reasoning items only with a `summary`
111 // list and silently ignores previous_response_id (verified live: a
112 // continuation request billed only the new-turn tokens and the model
113 // hallucinated unrelated context), so it is stateless like DeepSeek but
114 // needs the summary field like DashScope. Only step-3.7-flash is enabled
115 // server-side; the wire shape is identical on the standard and step_plan
116 // hosts, so one entry covers both.
117 "stepfun": {
118 stateless: true,
119 sessionCacheHeader: false,
120 toolCallReasoning: true,
121 singleSegmentReasoning: false,
122 ignoresTemperature: false,
123 summaryRequired: true,
124 compactionOutputTokens: provider.DefaultOrdinaryOutputTokens,
125 },
126 // "" (unknown OpenAI-compatible endpoint) → zero value = default behavior.
127 // Unknown gateways deliberately do NOT inherit a large max-output default.
128 }
129
130 // capabilitiesFor returns the wire capabilities for a detected vendor name.
131 // Unknown vendors fall back to the zero value (standard OpenAI behavior).
132 func capabilitiesFor(vendor string) vendorCapabilities {
133 return vendorTable[vendor]
134 }
135
136 // DetectVendor identifies endpoint behavior that affects the Responses wire.
137 // Empty means an unknown OpenAI-compatible endpoint with default behavior.
138 func DetectVendor(baseURL string) string {
139 u, err := url.Parse(strings.TrimSpace(baseURL))
140 if err != nil {
141 return ""
142 }
143 host := strings.ToLower(u.Hostname())
144 switch {
145 case host == "dashscope.aliyuncs.com", strings.HasSuffix(host, ".dashscope.aliyuncs.com"), strings.HasSuffix(host, ".maas.aliyuncs.com"):
146 return "dashscope"
147 case host == "api.deepseek.com", strings.HasSuffix(host, ".deepseek.com"):
148 return "deepseek"
149 case host == "api.xiaomimimo.com", strings.HasSuffix(host, ".xiaomimimo.com"):
150 return "mimo"
151 case host == "api.stepfun.com", host == "api.stepfun.ai":
152 return "stepfun"
153 default:
154 return ""
155 }
156 }
157
157 lines GO