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
**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.
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)
}
wireReq, err := openAIChatRequestFromGenerateRequest(req)
if err != nil {
return nil, fmt.Errorf("%w: %v", ErrInvalidRequest, err)
}
wirePayload, err := openAIChatRequestPayload(wireReq)
wirePayload, err := openAIChatRequestPayload(req)
if err != nil {
return nil, fmt.Errorf("%w: %v", ErrInvalidRequest, err)
}
@@ -159,7 +154,7 @@ func (c *OpenAICompatibleClient) PrepareGeneration(req domain.GenerateRequest) (
return &PreparedGeneration{
endpoint: endpoint, payload: payload, apiKey: apiKey,
timeoutSeconds: req.Target.TimeoutSeconds, httpClient: httpClient,
structuredOutputRequested: wireReq.ResponseFormat != nil,
structuredOutputRequested: wirePayload["response_format"] != 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)
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)
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 {
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 {
wireReq.Temperature = &req.Target.Temperature
out["temperature"] = req.Target.Temperature
}
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 {
wireReq.TopP = &req.Target.TopP
out["top_p"] = req.Target.TopP
}
if strings.TrimSpace(req.Target.ServiceTier) != "" {
wireReq.ServiceTier = req.Target.ServiceTier
out["service_tier"] = req.Target.ServiceTier
}
if strings.TrimSpace(req.Target.ReasoningEffort) != "" {
wireReq.ReasoningEffort = req.Target.ReasoningEffort
}
if len(req.Target.ExtraParams) > 0 {
wireReq.ExtraParams = req.Target.ExtraParams
out["reasoning_effort"] = req.Target.ReasoningEffort
}
if req.StructuredOutput != nil {
responseFormat, err := toOpenAIResponseFormat(req.StructuredOutput)
if err != nil {
return openAIChatRequest{}, err
return nil, err
}
if responseFormat != nil {
out["response_format"] = responseFormat
}
wireReq.ResponseFormat = responseFormat
}
return wireReq, nil
}
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 {
for key, value := range req.Target.ExtraParams {
if key == "" {
return nil, errors.New("extra_params key must not be empty")
}