ProcessRequest handles a request to /v1/chat/completions. See https://platform.openai.com/docs/api-reference/chat-streaming/streaming. It will inject any tools which have been provided by the [mcp.ServerProxier]. When a response from the server includes an event indicating that a tool must be invo
(w http.ResponseWriter, r *http.Request)
| 81 | // results relayed to the SERVER. The response from the server will be handled synchronously, and this loop |
| 82 | // can continue until all injected tool invocations are completed and the response is relayed to the client. |
| 83 | func (i *StreamingInterception) ProcessRequest(w http.ResponseWriter, r *http.Request) (outErr error) { |
| 84 | if i.req == nil { |
| 85 | return xerrors.New("developer error: req is nil") |
| 86 | } |
| 87 | |
| 88 | ctx, span := i.tracer.Start(r.Context(), "Intercept.ProcessRequest", trace.WithAttributes(tracing.InterceptionAttributesFromContext(r.Context())...)) |
| 89 | defer tracing.EndSpanErr(span, &outErr) |
| 90 | |
| 91 | // Include token usage. |
| 92 | i.req.StreamOptions.IncludeUsage = openai.Bool(true) |
| 93 | |
| 94 | i.injectTools() |
| 95 | |
| 96 | // Allow us to interrupt watch via cancel. |
| 97 | ctx, cancel := context.WithCancel(ctx) |
| 98 | defer cancel() |
| 99 | r = r.WithContext(ctx) // Rewire context for SSE cancellation. |
| 100 | |
| 101 | svc := i.newCompletionsService() |
| 102 | logger := i.logger.With(slog.F("model", i.req.Model)) |
| 103 | |
| 104 | streamCtx, streamCancel := context.WithCancelCause(ctx) |
| 105 | defer streamCancel(xerrors.New("deferred")) |
| 106 | |
| 107 | // events will either terminate when shutdown after interaction with upstream completes, or when streamCtx is done. |
| 108 | events := eventstream.NewEventStream(streamCtx, logger.Named("sse-sender"), nil) |
| 109 | go events.Start(w, r) |
| 110 | defer func() { |
| 111 | _ = events.Shutdown(streamCtx) // Catch-all in case it doesn't get shutdown after stream completes. |
| 112 | }() |
| 113 | |
| 114 | // Force responses to only have one choice. |
| 115 | // It's unnecessary to generate multiple responses, and would complicate our stream processing logic if |
| 116 | // multiple choices were returned. |
| 117 | i.req.N = openai.Int(1) |
| 118 | |
| 119 | prompt, err := i.req.lastUserPrompt() |
| 120 | if err != nil { |
| 121 | logger.Warn(ctx, "failed to retrieve last user prompt", slog.Error(err)) |
| 122 | } |
| 123 | |
| 124 | var ( |
| 125 | stream *ssestream.Stream[openai.ChatCompletionChunk] |
| 126 | lastErr error |
| 127 | interceptionErr error |
| 128 | ) |
| 129 | for { |
| 130 | // TODO add outer loop span (https://github.com/coder/aibridge/issues/67) |
| 131 | var opts []option.RequestOption |
| 132 | |
| 133 | // TODO(ssncferreira): inject actor headers directly in the client-header |
| 134 | // middleware instead of using SDK options. |
| 135 | if actor := aibcontext.ActorFromContext(r.Context()); actor != nil && i.cfg.SendActorHeaders { |
| 136 | opts = append(opts, intercept.ActorHeadersAsOpenAIOpts(actor)...) |
| 137 | } |
| 138 | |
| 139 | // We take control of request body here and pass it to the SDK as a raw byte slice. |
| 140 | // This is because the SDK's serialization applies hidden request options that result in |
nothing calls this directly
no test coverage detected