MCPcopy Index your code
hub / github.com/docker/docker-agent / DefaultMatcher

Function DefaultMatcher

pkg/fake/proxy.go:279–333  ·  view source on GitHub ↗

DefaultMatcher creates a matcher that normalizes dynamic fields for consistent matching. The onError callback is called if reading the request body fails (nil logs and returns false).

(onError func(err error))

Source from the content-addressed store, hash-verified

277// DefaultMatcher creates a matcher that normalizes dynamic fields for consistent matching.
278// The onError callback is called if reading the request body fails (nil logs and returns false).
279func DefaultMatcher(onError func(err error)) recorder.MatcherFunc {
280 // Normalize tool call IDs (they change between requests)
281 callIDRegex := regexp.MustCompile(`call_[a-z0-9\-]+`)
282 // Normalize max_tokens/max_output_tokens/maxOutputTokens field (varies based on models.dev
283 // cache state and provider cloning behavior). Handles both snake_case and camelCase variants.
284 maxTokensRegex := regexp.MustCompile(`"(?:max_(?:output_)?tokens|maxOutputTokens)":\d+,?`)
285 // Normalize Gemini thinkingConfig (varies based on provider defaults for thinking budget).
286 // This handles both camelCase (API) variants of the thinkingConfig field.
287 thinkingConfigRegex := regexp.MustCompile(`"thinkingConfig":\{[^}]*\},?`)
288 // Normalize OpenAI reasoning config (varies based on NoThinking flag and thinking budget).
289 reasoningRegex := regexp.MustCompile(`"reasoning":\{[^}]*\},?`)
290 // Normalize OpenAI tool_choice field. The string form ("auto", "none",
291 // "required") is now sent explicitly whenever tools are present so that
292 // strict gateways (LiteLLM) accept the request, but older cassettes were
293 // recorded without it.
294 toolChoiceRegex := regexp.MustCompile(`"tool_choice":"[^"]*",?`)
295
296 return func(r *http.Request, i cassette.Request) bool {
297 if r.Body == nil || r.Body == http.NoBody {
298 return cassette.DefaultMatcher(r, i)
299 }
300 if r.Method != i.Method {
301 return false
302 }
303 if r.URL.String() != i.URL {
304 return false
305 }
306
307 reqBody, err := io.ReadAll(r.Body)
308 if err != nil {
309 if onError != nil {
310 onError(err)
311 } else {
312 slog.Error("Failed to read request body for matching", "error", err)
313 }
314 return false
315 }
316 r.Body.Close()
317 r.Body = io.NopCloser(bytes.NewBuffer(reqBody))
318
319 // Normalize dynamic fields for matching
320 normalizedReq := callIDRegex.ReplaceAllString(string(reqBody), "call_ID")
321 normalizedReq = maxTokensRegex.ReplaceAllString(normalizedReq, "")
322 normalizedReq = thinkingConfigRegex.ReplaceAllString(normalizedReq, "")
323 normalizedReq = reasoningRegex.ReplaceAllString(normalizedReq, "")
324 normalizedReq = toolChoiceRegex.ReplaceAllString(normalizedReq, "")
325 normalizedCassette := callIDRegex.ReplaceAllString(i.Body, "call_ID")
326 normalizedCassette = maxTokensRegex.ReplaceAllString(normalizedCassette, "")
327 normalizedCassette = thinkingConfigRegex.ReplaceAllString(normalizedCassette, "")
328 normalizedCassette = reasoningRegex.ReplaceAllString(normalizedCassette, "")
329 normalizedCassette = toolChoiceRegex.ReplaceAllString(normalizedCassette, "")
330
331 return normalizedReq == normalizedCassette
332 }
333}
334
335// TargetURLForHost returns the target URL builder for a given forwarding host.
336// Returns nil if the host is not recognized.

Callers 3

startRecordingAIProxyFunction · 0.92
startReplayProxyFunction · 0.92
StartProxyWithOptionsFunction · 0.85

Calls 3

CloseMethod · 0.65
StringMethod · 0.45
ErrorMethod · 0.45

Tested by 2

startRecordingAIProxyFunction · 0.74
startReplayProxyFunction · 0.74