返回 DeepSeek-Reasonix
dto_provider.go
根目录 / internal / extension / protocol / dto_provider.go
1 package protocol
2
3 import (
4 "bytes"
5 "encoding/json"
6 "strings"
7 )
8
9 // Provider DTOs: the extension-hosted provider broker. The extension holds
10 // provider credentials and runs streams; the host only ever sees these
11 // credential-free public copies. Conversion to and from internal/provider
12 // types lives host-side in a later stage; these DTOs deliberately do not
13 // import internal/provider so the public wire schema stays self-contained.
14
15 // ProviderDescriptor mirrors provider.Descriptor field-for-field as a public
16 // DTO. It never carries endpoints, credentials, headers, or env names.
17 type ProviderDescriptor struct {
18 Ref string `json:"ref" validate:"nonempty"`
19 DisplayName string `json:"displayName,omitempty"`
20 Model string `json:"model,omitempty"`
21 ContextWindow int `json:"contextWindow,omitempty" validate:"min=0"`
22 PricingCurrency string `json:"pricingCurrency,omitempty"`
23 CacheHitPerMillion float64 `json:"cacheHitPerMillion,omitempty" validate:"min=0"`
24 InputPerMillion float64 `json:"inputPerMillion,omitempty" validate:"min=0"`
25 OutputPerMillion float64 `json:"outputPerMillion,omitempty" validate:"min=0"`
26 Vision bool `json:"vision,omitempty"`
27 InputModalities []string `json:"inputModalities,omitempty"`
28 Tools bool `json:"tools,omitempty"`
29 Reasoning bool `json:"reasoning,omitempty"`
30 Efforts []string `json:"efforts,omitempty"`
31 DefaultEffort string `json:"defaultEffort,omitempty"`
32 ToolCallReasoning bool `json:"toolCallReasoning,omitempty"`
33 ReasoningRoundTrip bool `json:"reasoningRoundTrip,omitempty"`
34 WarnOnMissingToolCallReasoning bool `json:"warnOnMissingToolCallReasoning,omitempty"`
35 }
36
37 // PluginRefOwner extracts the plugin ID from a plugin-namespaced provider ref
38 // (plugin/<pluginID>/<rest...>) — the namespace every extension-hosted
39 // provider ref carries. Anything else — including the two-segment
40 // "plugin/<model>" shape, which stays an ordinary host ref — returns "".
41 // Host layers (boot, config validation, frontends) use it to route plugin
42 // refs away from config-backed catalogs they can never appear in.
43 func PluginRefOwner(ref string) string {
44 rest, ok := strings.CutPrefix(ref, "plugin/")
45 if !ok {
46 return ""
47 }
48 pluginID, remainder, ok := strings.Cut(rest, "/")
49 if !ok || pluginID == "" || remainder == "" {
50 return ""
51 }
52 return pluginID
53 }
54
55 // ProviderMessage is the public copy of provider.Message. It keeps the same
56 // JSON field names (snake_case) so transcripts read identically, and drops
57 // the local-only UI metadata fields that never belong on the wire.
58 type ProviderMessage struct {
59 Role ProviderRole `json:"role,omitempty"`
60 Content string `json:"content,omitempty" externalizable:"true"`
61 Images []string `json:"images,omitempty"`
62 ReasoningContent string `json:"reasoning_content,omitempty"`
63 ReasoningSignature string `json:"reasoning_signature,omitempty"`
64 ToolCalls []ProviderToolCall `json:"tool_calls,omitempty"`
65 ToolCallID string `json:"tool_call_id,omitempty"`
66 Name string `json:"name,omitempty"`
67 }
68
69 // ProviderToolCall is the public copy of provider.ToolCall: provider-visible
70 // fields only, no Reasonix-local display metadata.
71 type ProviderToolCall struct {
72 ID string `json:"id" validate:"nonempty"`
73 Name string `json:"name" validate:"nonempty"`
74 Arguments string `json:"arguments"`
75 ThoughtSignature string `json:"thought_signature,omitempty"`
76 }
77
78 // ProviderToolSchema is the public copy of provider.ToolSchema. Parameters is
79 // a JSON Schema object.
80 type ProviderToolSchema struct {
81 Name string `json:"name" validate:"nonempty"`
82 Description string `json:"description,omitempty"`
83 Parameters json.RawMessage `json:"parameters"`
84 }
85
86 // ProviderResponseFormat asks an extension-hosted provider to constrain its
87 // output shape. It is optional so ordinary requests retain their existing,
88 // cache-stable wire representation.
89 type ProviderResponseFormat struct {
90 Type string `json:"type" validate:"nonempty"`
91 }
92
93 // ProviderRequest is the credential-free completion request the host asks the
94 // extension to stream. Nil Messages/Tools arrays are invalid; empty arrays
95 // are the canonical form.
96 type ProviderRequest struct {
97 Messages []ProviderMessage `json:"messages"`
98 Tools []ProviderToolSchema `json:"tools"`
99 Temperature *float64 `json:"temperature,omitempty"`
100 MaxTokens int `json:"maxTokens" validate:"min=0"`
101 ResponseFormat *ProviderResponseFormat `json:"responseFormat,omitempty"`
102 }
103
104 // Validate enforces the deterministic wire shape.
105 func (request ProviderRequest) Validate() error {
106 if request.Messages == nil || request.Tools == nil {
107 return validationError("messages and tools must be arrays")
108 }
109 if request.MaxTokens < 0 {
110 return validationError("maxTokens must be non-negative")
111 }
112 if request.ResponseFormat != nil && strings.TrimSpace(request.ResponseFormat.Type) == "" {
113 return validationError("responseFormat.type must be non-empty")
114 }
115 for _, tool := range request.Tools {
116 parameters := bytes.TrimSpace(tool.Parameters)
117 if len(parameters) == 0 || parameters[0] != '{' || !json.Valid(parameters) {
118 return validationError("tool parameters must be a JSON object")
119 }
120 }
121 return nil
122 }
123
124 // ProviderUsage is the public copy of provider.Usage token accounting.
125 type ProviderUsage struct {
126 PromptTokens int `json:"promptTokens" validate:"min=0"`
127 CompletionTokens int `json:"completionTokens" validate:"min=0"`
128 TotalTokens int `json:"totalTokens" validate:"min=0"`
129 CacheHitTokens int `json:"cacheHitTokens" validate:"min=0"`
130 CacheMissTokens int `json:"cacheMissTokens" validate:"min=0"`
131 ReasoningTokens int `json:"reasoningTokens" validate:"min=0"`
132 FinishReason string `json:"finishReason,omitempty"`
133 }
134
135 // ProviderError is deliberately generic, like the Remote broker's: raw
136 // provider errors can contain API keys, authorization headers, endpoints, or
137 // response bodies and must never cross the extension boundary.
138 type ProviderError struct {
139 Code ProviderErrorCode `json:"code"`
140 Message string `json:"message" validate:"nonempty"`
141 }
142
143 // ProviderChunk is one chunk of an extension-hosted provider stream.
144 type ProviderChunk struct {
145 Type ProviderChunkType `json:"type"`
146 Text string `json:"text,omitempty"`
147 Signature string `json:"signature,omitempty"`
148 ToolCall *ProviderToolCall `json:"toolCall,omitempty"`
149 ArgChars int `json:"argChars,omitempty" validate:"min=0"`
150 Usage *ProviderUsage `json:"usage,omitempty"`
151 Error *ProviderError `json:"error,omitempty"`
152 Generation uint64 `json:"generation,omitempty"`
153 Epoch string `json:"epoch,omitempty"`
154 }
155
156 // Validate enforces chunk invariants the tags cannot express.
157 func (chunk ProviderChunk) Validate() error {
158 if chunk.ArgChars < 0 {
159 return validationError("argChars must be non-negative")
160 }
161 if chunk.Type == ChunkError && chunk.Error == nil {
162 return validationError("error chunks require error")
163 }
164 if chunk.Type != ChunkError && chunk.Error != nil {
165 return validationError("non-error chunks forbid error")
166 }
167 if chunk.Type == ChunkUsage && chunk.Usage == nil {
168 return validationError("usage chunks require usage")
169 }
170 return nil
171 }
172
173 // ProviderCatalogParams asks for the extension's full provider catalog.
174 type ProviderCatalogParams struct{}
175
176 // ProviderCatalogResult is the extension's non-secret provider catalog.
177 type ProviderCatalogResult struct {
178 Providers []ProviderDescriptor `json:"providers"`
179 }
180
181 // StreamOpenParams opens one provider stream for a host turn. Chunks flow
182 // back as extension/provider/stream/chunk notifications numbered from
183 // SeqBase; the stream ends with exactly one stream/end notification.
184 type StreamOpenParams struct {
185 StreamID string `json:"streamId" validate:"nonempty"`
186 ProviderRef string `json:"providerRef" validate:"nonempty"`
187 Model string `json:"model,omitempty"`
188 Effort string `json:"effort,omitempty"`
189 Request ProviderRequest `json:"request"`
190 SeqBase int `json:"seqBase" validate:"min=0"`
191 Generation uint64 `json:"generation,omitempty"`
192 Epoch string `json:"epoch,omitempty"`
193 }
194
195 // Validate enforces required identifiers plus the request invariants.
196 func (p StreamOpenParams) Validate() error {
197 if strings.TrimSpace(p.StreamID) == "" || strings.TrimSpace(p.ProviderRef) == "" {
198 return validationError("streamId and providerRef are required")
199 }
200 return p.Request.Validate()
201 }
202
203 // StreamOpenResult acknowledges the stream; chunks arrive as notifications.
204 type StreamOpenResult struct {
205 Accepted bool `json:"accepted"`
206 }
207
208 // StreamCancelParams cancels one in-flight provider stream.
209 type StreamCancelParams struct {
210 StreamID string `json:"streamId" validate:"nonempty"`
211 }
212
213 // StreamCancelResult acknowledges the cancel.
214 type StreamCancelResult struct {
215 Cancelled bool `json:"cancelled"`
216 }
217
218 // StreamChunkParams is one provider chunk, Extension → Host.
219 type StreamChunkParams struct {
220 StreamID string `json:"streamId" validate:"nonempty"`
221 Seq int64 `json:"seq" validate:"min=1"`
222 Chunk ProviderChunk `json:"chunk"`
223 Generation uint64 `json:"generation,omitempty"`
224 Epoch string `json:"epoch,omitempty"`
225 }
226
227 // Validate enforces stream ordering preconditions and chunk invariants.
228 func (p StreamChunkParams) Validate() error {
229 if strings.TrimSpace(p.StreamID) == "" {
230 return validationError("streamId is required")
231 }
232 if p.Seq < 1 {
233 return validationError("seq must be >= 1")
234 }
235 return p.Chunk.Validate()
236 }
237
238 // StreamEndParams ends a stream, success or failure. LastSeq freezes the
239 // terminal ordering boundary: the receiver must hold chunks 1..LastSeq before
240 // completing the stream, and a missing chunk is a stream_gap error.
241 type StreamEndParams struct {
242 StreamID string `json:"streamId" validate:"nonempty"`
243 LastSeq int64 `json:"lastSeq" validate:"min=0"`
244 // Error is a redacted, non-secret failure message when the stream failed.
245 Error string `json:"error,omitempty"`
246 // Interrupted is true when the stream was cut mid-flight (transport drop
247 // or cancel), not when it finished or failed cleanly.
248 Interrupted bool `json:"interrupted,omitempty"`
249 }
250
250 lines GO