Build provider requests directly as payloads
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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")
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user