package gateway import ( "context" "strings" "github.com/enterpilot/gomodel/internal/core" "github.com/enterpilot/gomodel/internal/requestparams" ) // PrepareChatRequest resolves workflow/model policy and applies translated request patching. func (o *InferenceOrchestrator) PrepareChatRequest(ctx context.Context, req *core.ChatRequest, meta RequestMeta) (*PreparedChatRequest, error) { return prepareTranslated(o, ctx, req, meta, chatPrepareSpec) } // PrepareResponsesRequest resolves workflow/model policy and applies translated request patching. func (o *InferenceOrchestrator) PrepareResponsesRequest(ctx context.Context, req *core.ResponsesRequest, meta RequestMeta) (*PreparedResponsesRequest, error) { return prepareTranslated(o, ctx, req, meta, responsesPrepareSpec) } // PrepareEmbeddingRequest resolves workflow/model policy for an embeddings request. func (o *InferenceOrchestrator) PrepareEmbeddingRequest(ctx context.Context, req *core.EmbeddingRequest, meta RequestMeta) (*PreparedEmbeddingRequest, error) { if req == nil { return nil, core.NewInvalidRequestError("embeddings request is required", nil) } ctx = WithAttemptRecorder(contextWithRequestID(ctx, meta.RequestID)) workflow, err := o.ensureTranslatedRequestWorkflow(ctx, meta.Workflow, meta.RequestID, meta.Endpoint, &req.Model, &req.Provider) if err != nil { return nil, err } ctx = o.WithCacheRequestContext(ctx, workflow) return &PreparedEmbeddingRequest{Context: ctx, Request: req, Workflow: workflow}, nil } type translatedPrepareSpec[Req any, Prepared any] struct { requiredMessage string patchNilMessage string selector func(Req) (*string, *string) resolve func(*InferenceOrchestrator) func(context.Context, Req, *core.Workflow) (context.Context, Req, error) patch func(*InferenceOrchestrator) func(context.Context, Req) (Req, error) valid func(Req) bool // overrides applies the primary attempt's forced reasoning effort and // request parameters, so the prepared request (and the response cache key // built from it) carries what is actually sent upstream. The client's // request stays in the context so failover attempts are rebuilt from it // with their own overrides. overrides func(Req, requestparams.Overrides) (Req, error) build func(context.Context, Req, *core.Workflow) Prepared } var chatPrepareSpec = translatedPrepareSpec[*core.ChatRequest, *PreparedChatRequest]{ requiredMessage: "chat request is required", patchNilMessage: "patched chat request is required", selector: chatRequestSelector, patch: chatRequestPatch, valid: validChatRequest, overrides: ChatRequestWithOverrides, build: func(ctx context.Context, req *core.ChatRequest, workflow *core.Workflow) *PreparedChatRequest { return &PreparedChatRequest{Context: ctx, Request: req, Workflow: workflow} }, } var responsesPrepareSpec = translatedPrepareSpec[*core.ResponsesRequest, *PreparedResponsesRequest]{ requiredMessage: "responses request is required", patchNilMessage: "patched responses request is required", selector: responsesRequestSelector, resolve: responsesHistoryResolve, patch: responsesRequestPatch, valid: validResponsesRequest, overrides: ResponsesRequestWithOverrides, build: func(ctx context.Context, req *core.ResponsesRequest, workflow *core.Workflow) *PreparedResponsesRequest { return &PreparedResponsesRequest{Context: ctx, Request: req, Workflow: workflow} }, } func prepareTranslated[Req any, Prepared any]( o *InferenceOrchestrator, ctx context.Context, req Req, meta RequestMeta, spec translatedPrepareSpec[Req, Prepared], ) (Prepared, error) { var zero Prepared if !spec.valid(req) { return zero, core.NewInvalidRequestError(spec.requiredMessage, nil) } ctx = WithAttemptRecorder(ctx) model, provider := spec.selector(req) var resolve func(context.Context, Req, *core.Workflow) (context.Context, Req, error) if spec.resolve != nil { resolve = spec.resolve(o) } ctx, req, workflow, err := prepareTranslatedRequest(o, ctx, req, meta, model, provider, resolve, spec.patch(o), spec.valid, spec.patchNilMessage) if err != nil { if workflow != nil { // A patch-phase error (a guardrail block or short-circuit) still // reports the resolved workflow so the caller can render the // outcome with routing context. return spec.build(ctx, req, workflow), err } return zero, err } if spec.overrides != nil { if overrides := o.RequestOverrides(ctx, workflow); !overrides.IsZero() { ctx = withClientRequest(ctx, req) if req, err = spec.overrides(req, overrides); err != nil { return zero, overridesError(err) } } } return spec.build(ctx, req, workflow), nil } func prepareTranslatedRequest[Req any]( o *InferenceOrchestrator, ctx context.Context, req Req, meta RequestMeta, model, provider *string, resolve func(context.Context, Req, *core.Workflow) (context.Context, Req, error), patch func(context.Context, Req) (Req, error), valid func(Req) bool, patchNilMessage string, ) (context.Context, Req, *core.Workflow, error) { ctx = contextWithRequestID(ctx, meta.RequestID) workflow, err := o.ensureTranslatedRequestWorkflow(ctx, meta.Workflow, meta.RequestID, meta.Endpoint, model, provider) if err != nil { var zero Req return ctx, zero, nil, err } ctx = core.WithWorkflow(ctx, workflow) if resolve != nil { ctx, req, err = resolve(ctx, req, workflow) if err != nil { var zero Req return ctx, zero, workflow, err } } if patch != nil { req, err = patch(ctx, req) if err != nil { var zero Req return ctx, zero, workflow, err } if valid != nil && !valid(req) { var zero Req return ctx, zero, nil, core.NewInvalidRequestError(patchNilMessage, nil) } } ctx = o.WithCacheRequestContext(ctx, workflow) return ctx, req, workflow, nil } func validChatRequest(req *core.ChatRequest) bool { return req != nil } func validResponsesRequest(req *core.ResponsesRequest) bool { return req != nil } func chatRequestSelector(req *core.ChatRequest) (*string, *string) { return &req.Model, &req.Provider } func responsesRequestSelector(req *core.ResponsesRequest) (*string, *string) { return &req.Model, &req.Provider } func chatRequestPatch(o *InferenceOrchestrator) func(context.Context, *core.ChatRequest) (*core.ChatRequest, error) { if o.translatedRequestPatcher == nil { return nil } return o.translatedRequestPatcher.PatchChatRequest } func responsesRequestPatch(o *InferenceOrchestrator) func(context.Context, *core.ResponsesRequest) (*core.ResponsesRequest, error) { if o.translatedRequestPatcher == nil { return nil } return o.translatedRequestPatcher.PatchResponsesRequest } // responsesHistoryResolve expands a Responses request's history before the // prompt phase, telling the resolver every provider type an attempt of the // request may reach. func responsesHistoryResolve(o *InferenceOrchestrator) func(context.Context, *core.ResponsesRequest, *core.Workflow) (context.Context, *core.ResponsesRequest, error) { if o.responsesHistoryResolver == nil { return nil } return func(ctx context.Context, req *core.ResponsesRequest, workflow *core.Workflow) (context.Context, *core.ResponsesRequest, error) { types := []string{o.ProviderTypeForSelector(core.ModelSelector{Model: req.Model, Provider: req.Provider}, ProviderTypeFromWorkflow(workflow))} for _, selector := range o.FailoverSelectors(workflow) { types = append(types, o.ProviderTypeForSelector(selector, "")) } return o.responsesHistoryResolver.ResolveResponsesHistory(ctx, req, types) } } func contextWithRequestID(ctx context.Context, requestID string) context.Context { if ctx == nil { ctx = context.Background() } requestID = strings.TrimSpace(requestID) if requestID == "" || strings.TrimSpace(core.GetRequestID(ctx)) == requestID { return ctx } return core.WithRequestID(ctx, requestID) } func (o *InferenceOrchestrator) ensureTranslatedRequestWorkflow( ctx context.Context, current *core.Workflow, requestID string, endpoint core.EndpointDescriptor, model, providerHint *string, ) (*core.Workflow, error) { if model == nil || providerHint == nil { return nil, core.NewInvalidRequestError("model selector targets are required", nil) } if ctx == nil { ctx = context.Background() } workflow := currentTranslatedWorkflow(current, endpoint) var err error if workflow == nil { resolution, err := ResolveRequestModelWithAuthorizer(ctx, o.provider, o.modelResolver, o.modelAuthorizer, core.NewRequestedModelSelector(*model, *providerHint)) if err != nil { return nil, err } workflow, err = TranslatedWorkflow(ctx, strings.TrimSpace(requestID), endpoint, resolution, o.workflowPolicyResolver) if err != nil { return nil, err } ApplyResolvedSelector(model, providerHint, resolution) return workflow, nil } resolution := workflow.Resolution if resolution != nil && o.modelAuthorizer != nil { if err := o.modelAuthorizer.ValidateModelAccess(ctx, resolution.ResolvedSelector); err != nil { return nil, err } } if resolution == nil { resolution, err = ResolveRequestModelWithAuthorizer(ctx, o.provider, o.modelResolver, o.modelAuthorizer, core.NewRequestedModelSelector(*model, *providerHint)) if err != nil { return nil, err } workflow, err = TranslatedWorkflow(ctx, strings.TrimSpace(requestID), endpoint, resolution, o.workflowPolicyResolver) if err != nil { return nil, err } } ApplyResolvedSelector(model, providerHint, resolution) return workflow, nil } func currentTranslatedWorkflow(workflow *core.Workflow, endpoint core.EndpointDescriptor) *core.Workflow { if workflow == nil { return nil } if workflow.Mode != core.ExecutionModeTranslated || workflow.Endpoint.Operation != endpoint.Operation { return nil } return workflow } // ApplyResolvedSelector updates request model/provider fields to the resolved selector. func ApplyResolvedSelector(model, providerHint *string, resolution *core.RequestModelResolution) { if model == nil || providerHint == nil || resolution == nil { return } *model = resolution.ResolvedSelector.Model *providerHint = resolution.ResolvedSelector.Provider } // WithCacheRequestContext injects workflow and guardrails cache metadata into ctx. func (o *InferenceOrchestrator) WithCacheRequestContext(ctx context.Context, workflow *core.Workflow) context.Context { if ctx == nil { ctx = context.Background() } if workflow != nil { ctx = core.WithWorkflow(ctx, workflow) } if workflow != nil && workflow.Policy != nil { return core.WithGuardrailsHash(ctx, workflow.GuardrailsHash()) } if o.guardrailsHash != "" { return core.WithGuardrailsHash(ctx, o.guardrailsHash) } return ctx }