134 lines
5 KiB
Go
134 lines
5 KiB
Go
package openai
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
func decodeChatCompletionRequest(dec *json.Decoder, req *chatCompletionRequest) error {
|
|
var raw map[string]json.RawMessage
|
|
if err := dec.Decode(&raw); err != nil {
|
|
return fmt.Errorf("invalid JSON request")
|
|
}
|
|
for key := range raw {
|
|
switch key {
|
|
case "model", "messages", "stream", "metadata", "max_tokens", "max_completion_tokens", "temperature", "top_p", "presence_penalty", "frequency_penalty", "seed", "stop", "response_format", "tools", "tool_choice", "parallel_tool_calls", "stream_options", "store", "think", "reasoning_effort", "thinking_token_budget", "include_reasoning":
|
|
default:
|
|
return fmt.Errorf("%s is not supported for /v1/chat/completions", key)
|
|
}
|
|
}
|
|
normalized, err := json.Marshal(raw)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid JSON request")
|
|
}
|
|
if err := json.Unmarshal(normalized, req); err != nil {
|
|
return fmt.Errorf("invalid /v1/chat/completions request format")
|
|
}
|
|
if req.MaxTokens != nil && *req.MaxTokens <= 0 {
|
|
return fmt.Errorf("max_tokens must be greater than zero")
|
|
}
|
|
if req.MaxCompletionTokens != nil && *req.MaxCompletionTokens <= 0 {
|
|
return fmt.Errorf("max_completion_tokens must be greater than zero")
|
|
}
|
|
if req.Temperature != nil && (*req.Temperature < 0 || *req.Temperature > 2) {
|
|
return fmt.Errorf("temperature must be between 0 and 2")
|
|
}
|
|
if req.TopP != nil && (*req.TopP < 0 || *req.TopP > 1) {
|
|
return fmt.Errorf("top_p must be between 0 and 1")
|
|
}
|
|
if err := validateThinkControl(req); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// chatCompletionEnvelope holds the routing-relevant fields decoded leniently
|
|
// from a /v1/chat/completions request body before strict normalization. Unknown
|
|
// fields are ignored so the provider tunnel passthrough can forward Codex/
|
|
// provider-specific payloads verbatim. Strict field validation stays on the
|
|
// normalized non-provider path.
|
|
type chatCompletionEnvelope struct {
|
|
Model string `json:"model"`
|
|
Metadata json.RawMessage `json:"metadata,omitempty"`
|
|
Stream bool `json:"stream"`
|
|
}
|
|
|
|
// decodeChatCompletionEnvelope leniently extracts only the routing-relevant
|
|
// fields (model, metadata, stream) from a /v1/chat/completions request body.
|
|
// It uses json.Decoder to read only the first JSON value so caller-provided
|
|
// trailing payload (provider-specific extensions beyond the root object) does
|
|
// not invalidate the envelope; strict field validation stays on the normalized
|
|
// non-provider path.
|
|
func decodeChatCompletionEnvelope(rawBody []byte) (chatCompletionEnvelope, error) {
|
|
var env chatCompletionEnvelope
|
|
dec := json.NewDecoder(bytes.NewReader(rawBody))
|
|
if err := dec.Decode(&env); err != nil {
|
|
return chatCompletionEnvelope{}, fmt.Errorf("invalid JSON request")
|
|
}
|
|
return env, nil
|
|
}
|
|
|
|
// decodeChatCompletionRequestLenient decodes a /v1/chat/completions request
|
|
// body while preserving unknown top-level fields. Known-field syntax and
|
|
// validation are still enforced so malformed standard fields cannot reach the
|
|
// provider tunnel. This is the provider-pool ingress path.
|
|
func decodeChatCompletionRequestLenient(dec *json.Decoder, req *chatCompletionRequest) error {
|
|
var raw map[string]json.RawMessage
|
|
if err := dec.Decode(&raw); err != nil {
|
|
return fmt.Errorf("invalid JSON request")
|
|
}
|
|
for key := range raw {
|
|
switch key {
|
|
case "model", "messages", "stream", "metadata", "max_tokens", "max_completion_tokens", "temperature", "top_p", "presence_penalty", "frequency_penalty", "seed", "stop", "response_format", "tools", "tool_choice", "parallel_tool_calls", "stream_options", "store", "think", "reasoning_effort", "thinking_token_budget", "include_reasoning":
|
|
default:
|
|
// Unknown fields are tolerated for provider-pool passthrough.
|
|
}
|
|
}
|
|
normalized, err := json.Marshal(raw)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid JSON request")
|
|
}
|
|
if err := json.Unmarshal(normalized, req); err != nil {
|
|
return fmt.Errorf("invalid /v1/chat/completions request format")
|
|
}
|
|
// Reuse the same validation rules on known fields for consistency.
|
|
if req.MaxTokens != nil && *req.MaxTokens <= 0 {
|
|
return fmt.Errorf("max_tokens must be greater than zero")
|
|
}
|
|
if req.MaxCompletionTokens != nil && *req.MaxCompletionTokens <= 0 {
|
|
return fmt.Errorf("max_completion_tokens must be greater than zero")
|
|
}
|
|
if req.Temperature != nil && (*req.Temperature < 0 || *req.Temperature > 2) {
|
|
return fmt.Errorf("temperature must be between 0 and 2")
|
|
}
|
|
if req.TopP != nil && (*req.TopP < 0 || *req.TopP > 1) {
|
|
return fmt.Errorf("top_p must be between 0 and 1")
|
|
}
|
|
if err := validateThinkControl(req); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func promptFromMessages(messages []chatMessage) string {
|
|
var b strings.Builder
|
|
for _, msg := range messages {
|
|
content := strings.TrimSpace(msg.Content)
|
|
if content == "" {
|
|
continue
|
|
}
|
|
role := strings.TrimSpace(msg.Role)
|
|
if role == "" {
|
|
role = "user"
|
|
}
|
|
if b.Len() > 0 {
|
|
b.WriteString("\n")
|
|
}
|
|
b.WriteString(role)
|
|
b.WriteString(": ")
|
|
b.WriteString(content)
|
|
}
|
|
return b.String()
|
|
}
|