@@ -33,23 +33,25 @@ func NewHTTPHandler(log logging.Logger, schedulerHTTP http.Handler, allowedOrigi
3333 maxRequestBodyBytes : 10 * 1024 * 1024 , // Default to 10MB
3434 }
3535
36- // Register routes
37- h .router .HandleFunc ("POST " + APIPrefix , h .handleCreate )
38- h .router .HandleFunc ("GET " + APIPrefix + "/{id}" , h .handleGet )
39- h .router .HandleFunc ("GET " + APIPrefix + "/{id}/input_items" , h .handleListInputItems )
40- h .router .HandleFunc ("DELETE " + APIPrefix + "/{id}" , h .handleDelete )
41- // Also register /v1/responses routes
42- h .router .HandleFunc ("POST /v1" + APIPrefix , h .handleCreate )
43- h .router .HandleFunc ("GET /v1" + APIPrefix + "/{id}" , h .handleGet )
44- h .router .HandleFunc ("GET /v1" + APIPrefix + "/{id}/input_items" , h .handleListInputItems )
45- h .router .HandleFunc ("DELETE /v1" + APIPrefix + "/{id}" , h .handleDelete )
36+ h .registerRoutes (APIPrefix )
37+ h .registerRoutes ("/v1" + APIPrefix )
38+ h .registerRoutes ("/engines" + APIPrefix )
39+ h .registerRoutes ("/engines/v1" + APIPrefix )
40+ h .registerRoutes ("/engines/{backend}/v1" + APIPrefix )
4641
4742 // Apply CORS middleware
4843 h .httpHandler = middleware .CorsMiddleware (allowedOrigins , h .router )
4944
5045 return h
5146}
5247
48+ func (h * HTTPHandler ) registerRoutes (prefix string ) {
49+ h .router .HandleFunc ("POST " + prefix , h .handleCreate )
50+ h .router .HandleFunc ("GET " + prefix + "/{id}" , h .handleGet )
51+ h .router .HandleFunc ("GET " + prefix + "/{id}/input_items" , h .handleListInputItems )
52+ h .router .HandleFunc ("DELETE " + prefix + "/{id}" , h .handleDelete )
53+ }
54+
5355// Close releases resources held by the handler, including the background
5456// store cleanup goroutine. It should be called when the handler is shut down.
5557func (h * HTTPHandler ) Close () {
@@ -97,14 +99,21 @@ func (h *HTTPHandler) handleCreate(w http.ResponseWriter, r *http.Request) {
9799 h .sendError (w , http .StatusBadRequest , "invalid_request" , "model is required" )
98100 return
99101 }
102+ if err := validateUnsupportedRequestFields (& req ); err != nil {
103+ h .sendError (w , http .StatusBadRequest , "invalid_request" , err .Error ())
104+ return
105+ }
100106
101107 // Create a new response
102108 respID := GenerateResponseID ()
103109 resp := NewResponse (respID , req .Model )
110+ store := shouldStore (& req )
104111 resp .Instructions = nilIfEmpty (req .Instructions )
105112 resp .Temperature = req .Temperature
106113 resp .TopP = req .TopP
107114 resp .MaxOutputTokens = req .MaxOutputTokens
115+ resp .Store = & store
116+ resp .Text = req .Text
108117 resp .Tools = req .Tools
109118 resp .ToolChoice = req .ToolChoice
110119 resp .ParallelToolCalls = req .ParallelToolCalls
@@ -134,7 +143,7 @@ func (h *HTTPHandler) handleCreate(w http.ResponseWriter, r *http.Request) {
134143 }
135144
136145 // Create upstream request
137- upstreamReq , err := http .NewRequestWithContext (r .Context (), http .MethodPost , "/engines/v1/chat/completions" , bytes .NewReader (chatBody ))
146+ upstreamReq , err := http .NewRequestWithContext (r .Context (), http .MethodPost , chatCompletionPathForRequest ( r ) , bytes .NewReader (chatBody ))
138147 if err != nil {
139148 h .sendError (w , http .StatusInternalServerError , "internal_error" , "Failed to create request" )
140149 return
@@ -147,24 +156,29 @@ func (h *HTTPHandler) handleCreate(w http.ResponseWriter, r *http.Request) {
147156
148157 if req .Stream {
149158 // Handle streaming response
150- h .handleStreaming (w , upstreamReq , resp )
159+ h .handleStreaming (w , upstreamReq , resp , store )
151160 } else {
152161 // Handle non-streaming response
153- h .handleNonStreaming (w , upstreamReq , resp )
162+ h .handleNonStreaming (w , upstreamReq , resp , store )
154163 }
155164}
156165
157166// handleStreaming handles streaming responses.
158- func (h * HTTPHandler ) handleStreaming (w http.ResponseWriter , upstreamReq * http.Request , resp * Response ) {
167+ func (h * HTTPHandler ) handleStreaming (w http.ResponseWriter , upstreamReq * http.Request , resp * Response , store bool ) {
168+ responseStore := h .store
169+ if ! store {
170+ responseStore = nil
171+ }
172+
159173 // Create streaming writer
160- streamWriter := NewStreamingResponseWriter (w , resp , h . store )
174+ streamWriter := NewStreamingResponseWriter (w , resp , responseStore )
161175
162176 // Forward to scheduler
163177 h .schedulerHTTP .ServeHTTP (streamWriter , upstreamReq )
164178}
165179
166180// handleNonStreaming handles non-streaming responses.
167- func (h * HTTPHandler ) handleNonStreaming (w http.ResponseWriter , upstreamReq * http.Request , resp * Response ) {
181+ func (h * HTTPHandler ) handleNonStreaming (w http.ResponseWriter , upstreamReq * http.Request , resp * Response , store bool ) {
168182 // Capture upstream response
169183 capture := NewNonStreamingResponseCapture ()
170184
@@ -187,7 +201,9 @@ func (h *HTTPHandler) handleNonStreaming(w http.ResponseWriter, upstreamReq *htt
187201 Code : errResp .Error .Code ,
188202 Message : errResp .Error .Message ,
189203 }
190- h .store .Save (resp )
204+ if store {
205+ h .store .Save (resp )
206+ }
191207 h .sendJSON (w , capture .StatusCode , resp )
192208 return
193209 }
@@ -197,7 +213,9 @@ func (h *HTTPHandler) handleNonStreaming(w http.ResponseWriter, upstreamReq *htt
197213 Code : "upstream_error" ,
198214 Message : capture .Body .String (),
199215 }
200- h .store .Save (resp )
216+ if store {
217+ h .store .Save (resp )
218+ }
201219 h .sendJSON (w , capture .StatusCode , resp )
202220 return
203221 }
@@ -210,7 +228,9 @@ func (h *HTTPHandler) handleNonStreaming(w http.ResponseWriter, upstreamReq *htt
210228 Code : "parse_error" ,
211229 Message : "Failed to parse upstream response" ,
212230 }
213- h .store .Save (resp )
231+ if store {
232+ h .store .Save (resp )
233+ }
214234 h .sendJSON (w , http .StatusInternalServerError , resp )
215235 return
216236 }
@@ -222,6 +242,8 @@ func (h *HTTPHandler) handleNonStreaming(w http.ResponseWriter, upstreamReq *htt
222242 finalResp .Temperature = resp .Temperature
223243 finalResp .TopP = resp .TopP
224244 finalResp .MaxOutputTokens = resp .MaxOutputTokens
245+ finalResp .Store = resp .Store
246+ finalResp .Text = resp .Text
225247 finalResp .Tools = resp .Tools
226248 finalResp .ToolChoice = resp .ToolChoice
227249 finalResp .ParallelToolCalls = resp .ParallelToolCalls
@@ -232,7 +254,9 @@ func (h *HTTPHandler) handleNonStreaming(w http.ResponseWriter, upstreamReq *htt
232254 finalResp .CreatedAt = resp .CreatedAt
233255
234256 // Store the response
235- h .store .Save (finalResp )
257+ if store {
258+ h .store .Save (finalResp )
259+ }
236260
237261 // Send response
238262 h .sendJSON (w , http .StatusOK , finalResp )
@@ -332,6 +356,58 @@ func nilIfEmpty(s string) *string {
332356 return & s
333357}
334358
359+ func shouldStore (req * CreateRequest ) bool {
360+ return req .Store == nil || * req .Store
361+ }
362+
363+ func chatCompletionPathForRequest (r * http.Request ) string {
364+ if backend := r .PathValue ("backend" ); backend != "" {
365+ return "/engines/" + backend + "/v1/chat/completions"
366+ }
367+ return "/engines/v1/chat/completions"
368+ }
369+
370+ func validateUnsupportedRequestFields (req * CreateRequest ) error {
371+ if len (req .Include ) > 0 {
372+ return unsupportedFieldError ("include" )
373+ }
374+ if req .StreamOptions != nil && ! isNullJSON (req .StreamOptions ) {
375+ return unsupportedFieldError ("stream_options" )
376+ }
377+ if req .TopLogprobs != nil {
378+ return unsupportedFieldError ("top_logprobs" )
379+ }
380+ switch req .Truncation {
381+ case "" , "disabled" :
382+ default :
383+ return fmt .Errorf ("truncation value %q is not supported by Docker Model Runner Responses compatibility layer" , req .Truncation )
384+ }
385+ if req .Background != nil && * req .Background {
386+ return fmt .Errorf ("background responses are not supported by Docker Model Runner Responses compatibility layer" )
387+ }
388+ if req .Conversation != nil && ! isNullJSON (req .Conversation ) {
389+ return fmt .Errorf ("conversation is not supported by Docker Model Runner Responses compatibility layer; use previous_response_id instead" )
390+ }
391+ if req .Prompt != nil && ! isNullJSON (req .Prompt ) {
392+ return unsupportedFieldError ("prompt" )
393+ }
394+ if req .ServiceTier != "" {
395+ return unsupportedFieldError ("service_tier" )
396+ }
397+ if req .SafetyIdentifier != "" {
398+ return unsupportedFieldError ("safety_identifier" )
399+ }
400+ return nil
401+ }
402+
403+ func unsupportedFieldError (field string ) error {
404+ return fmt .Errorf ("%s is not supported by Docker Model Runner Responses compatibility layer" , field )
405+ }
406+
407+ func isNullJSON (raw json.RawMessage ) bool {
408+ return strings .TrimSpace (string (raw )) == "null"
409+ }
410+
335411// GetStore returns the response store (for testing).
336412func (h * HTTPHandler ) GetStore () * Store {
337413 return h .store
0 commit comments