Build provider requests directly as payloads

This commit is contained in:
2026-11-05 19:53:41 +00:00
parent 448e64dc40
commit 0dec3dcec0
2 changed files with 26 additions and 70 deletions

View File

@@ -97,6 +97,11 @@ where fixture migration remains. No production construction supplies the removed
## Stage 2: Encode One Root Wire Representation ## Stage 2: Encode One Root Wire Representation
**Completed:** Requests now build the provider payload directly, with structured
dispatch evidence derived from that payload. Go 1.27.3 `go test ./internal/llm
./internal/generation` passed, including mapping, presence, payload freezing,
session, structured-output, and encoding-failure tests.
**Finding:** 2. **Files:** `internal/llm/openai_compatible_client.go` and request-mapping tests. **Finding:** 2. **Files:** `internal/llm/openai_compatible_client.go` and request-mapping tests.
1. Replace the root `domain request -> openAIChatRequest -> map` sequence with 1. Replace the root `domain request -> openAIChatRequest -> map` sequence with

View File

@@ -125,12 +125,7 @@ func (c *OpenAICompatibleClient) PrepareGeneration(req domain.GenerateRequest) (
return nil, fmt.Errorf("%w: invalid endpoint path: %v", ErrInvalidRequest, err) return nil, fmt.Errorf("%w: invalid endpoint path: %v", ErrInvalidRequest, err)
} }
wireReq, err := openAIChatRequestFromGenerateRequest(req) wirePayload, err := openAIChatRequestPayload(req)
if err != nil {
return nil, fmt.Errorf("%w: %v", ErrInvalidRequest, err)
}
wirePayload, err := openAIChatRequestPayload(wireReq)
if err != nil { if err != nil {
return nil, fmt.Errorf("%w: %v", ErrInvalidRequest, err) return nil, fmt.Errorf("%w: %v", ErrInvalidRequest, err)
} }
@@ -159,7 +154,7 @@ func (c *OpenAICompatibleClient) PrepareGeneration(req domain.GenerateRequest) (
return &PreparedGeneration{ return &PreparedGeneration{
endpoint: endpoint, payload: payload, apiKey: apiKey, endpoint: endpoint, payload: payload, apiKey: apiKey,
timeoutSeconds: req.Target.TimeoutSeconds, httpClient: httpClient, timeoutSeconds: req.Target.TimeoutSeconds, httpClient: httpClient,
structuredOutputRequested: wireReq.ResponseFormat != nil, structuredOutputRequested: wirePayload["response_format"] != nil,
}, nil }, nil
} }
@@ -425,96 +420,52 @@ func openAIChatResponseTooLargeError() error {
) )
} }
func openAIChatRequestFromGenerateRequest(req domain.GenerateRequest) (openAIChatRequest, error) { func openAIChatRequestPayload(req domain.GenerateRequest) (map[string]any, error) {
model := strings.TrimSpace(req.Target.Model) model := strings.TrimSpace(req.Target.Model)
if model == "" { if model == "" {
return openAIChatRequest{}, errors.New("model is required") return nil, errors.New("model is required")
} }
wireReq := openAIChatRequest{
Model: model,
}
sessionID, err := domain.NormalizeSessionID(req.Prompt.SessionID) sessionID, err := domain.NormalizeSessionID(req.Prompt.SessionID)
if err != nil { if err != nil {
return openAIChatRequest{}, err return nil, err
} }
wireReq.SessionID = sessionID
wireReq.Messages = make([]openAIChatRequestMessage, 0, len(req.Prompt.Messages)) messages := make([]openAIChatRequestMessage, 0, len(req.Prompt.Messages))
for _, msg := range req.Prompt.Messages { for _, msg := range req.Prompt.Messages {
wireReq.Messages = append(wireReq.Messages, openAIChatRequestMessageFromRenderedMessage(msg)) messages = append(messages, openAIChatRequestMessageFromRenderedMessage(msg))
} }
out := map[string]any{"model": model, "messages": messages}
if sessionID != "" {
out["session_id"] = sessionID
}
if req.Target.Temperature != 0 || req.TargetPresence.Temperature { if req.Target.Temperature != 0 || req.TargetPresence.Temperature {
wireReq.Temperature = &req.Target.Temperature out["temperature"] = req.Target.Temperature
} }
if req.Target.MaxTokens != 0 || req.TargetPresence.MaxTokens { if req.Target.MaxTokens != 0 || req.TargetPresence.MaxTokens {
wireReq.MaxTokens = &req.Target.MaxTokens out["max_tokens"] = req.Target.MaxTokens
} }
if req.Target.TopP != 0 || req.TargetPresence.TopP { if req.Target.TopP != 0 || req.TargetPresence.TopP {
wireReq.TopP = &req.Target.TopP out["top_p"] = req.Target.TopP
} }
if strings.TrimSpace(req.Target.ServiceTier) != "" { if strings.TrimSpace(req.Target.ServiceTier) != "" {
wireReq.ServiceTier = req.Target.ServiceTier out["service_tier"] = req.Target.ServiceTier
} }
if strings.TrimSpace(req.Target.ReasoningEffort) != "" { if strings.TrimSpace(req.Target.ReasoningEffort) != "" {
wireReq.ReasoningEffort = req.Target.ReasoningEffort out["reasoning_effort"] = req.Target.ReasoningEffort
}
if len(req.Target.ExtraParams) > 0 {
wireReq.ExtraParams = req.Target.ExtraParams
} }
if req.StructuredOutput != nil { if req.StructuredOutput != nil {
responseFormat, err := toOpenAIResponseFormat(req.StructuredOutput) responseFormat, err := toOpenAIResponseFormat(req.StructuredOutput)
if err != nil { if err != nil {
return openAIChatRequest{}, err return nil, err
}
if responseFormat != nil {
out["response_format"] = responseFormat
} }
wireReq.ResponseFormat = responseFormat
} }
return wireReq, nil for key, value := range req.Target.ExtraParams {
}
type openAIChatRequest struct {
Model string `json:"model"`
SessionID string `json:"session_id,omitempty"`
Messages []openAIChatRequestMessage `json:"messages"`
Temperature *float64 `json:"temperature,omitempty"`
MaxTokens *int `json:"max_tokens,omitempty"`
TopP *float64 `json:"top_p,omitempty"`
ServiceTier string `json:"service_tier,omitempty"`
ReasoningEffort string `json:"reasoning_effort,omitempty"`
ResponseFormat *openAIResponseFormat `json:"response_format,omitempty"`
ExtraParams map[string]any `json:"-"`
}
func openAIChatRequestPayload(req openAIChatRequest) (map[string]any, error) {
out := map[string]any{
"model": req.Model,
"messages": req.Messages,
}
if req.SessionID != "" {
out["session_id"] = req.SessionID
}
if req.Temperature != nil {
out["temperature"] = *req.Temperature
}
if req.MaxTokens != nil {
out["max_tokens"] = *req.MaxTokens
}
if req.TopP != nil {
out["top_p"] = *req.TopP
}
if req.ServiceTier != "" {
out["service_tier"] = req.ServiceTier
}
if req.ReasoningEffort != "" {
out["reasoning_effort"] = req.ReasoningEffort
}
if req.ResponseFormat != nil {
out["response_format"] = req.ResponseFormat
}
for key, value := range req.ExtraParams {
if key == "" { if key == "" {
return nil, errors.New("extra_params key must not be empty") return nil, errors.New("extra_params key must not be empty")
} }