package openai import ( "bytes" "encoding/json" "fmt" "strings" "iop/packages/go/streamgate" ) 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 } // decodeOpenAIChatRepeatHistory builds the Chat-specific, raw-free repeat // history view. Only plain role text, the documented reasoning aliases, and // completed tool call/result pairs participate. Signed, encrypted, malformed, // and unknown fields remain in the canonical ingress snapshot but are ignored // by this semantic decoder. func decodeOpenAIChatRepeatHistory(rawBody []byte) (openAIRepeatHistorySnapshot, error) { var root struct { Messages []json.RawMessage `json:"messages"` } if err := json.Unmarshal(rawBody, &root); err != nil { return openAIRepeatHistorySnapshot{}, fmt.Errorf("decode Chat repeat history: %w", err) } builder := newOpenAIRepeatHistoryBuilder() pendingActions := make(map[string]streamgate.FixedFingerprint) for _, rawMessage := range root.Messages { var message map[string]json.RawMessage if err := json.Unmarshal(rawMessage, &message); err != nil { continue } var role string if err := json.Unmarshal(message["role"], &role); err != nil { continue } role = strings.ToLower(strings.TrimSpace(role)) for _, text := range openAIRepeatPlainTextParts(message["content"]) { builder.recordText(role, openAIRepeatChannelContent, text) } for _, alias := range []string{"reasoning_content", "reasoning", "reasoning_text"} { var reasoning string if err := json.Unmarshal(message[alias], &reasoning); err == nil { builder.recordText(role, openAIRepeatChannelReasoning, reasoning) } } if role == "assistant" { var calls []json.RawMessage if json.Unmarshal(message["tool_calls"], &calls) == nil { for _, rawCall := range calls { var call struct { ID string `json:"id"` Type string `json:"type"` Function struct { Name string `json:"name"` Arguments json.RawMessage `json:"arguments"` } `json:"function"` } if json.Unmarshal(rawCall, &call) != nil { continue } if fingerprint, ok := openAIRepeatActionFingerprint( call.Function.Name, openAIRepeatArgumentsValue(call.Function.Arguments), ); ok && strings.TrimSpace(call.ID) != "" { pendingActions[call.ID] = fingerprint } } } } if role == "tool" { var callID string if json.Unmarshal(message["tool_call_id"], &callID) != nil { continue } action, ok := pendingActions[callID] if !ok { continue } result, ok := openAIRepeatTextFingerprint(strings.Join(openAIRepeatPlainTextParts(message["content"]), "\n")) if ok { builder.recordCompletedAction(action, result) } } } return builder.snapshot(), nil } func openAIRepeatArgumentsValue(raw json.RawMessage) json.RawMessage { var value string if json.Unmarshal(raw, &value) == nil { return json.RawMessage(value) } return raw } func openAIRepeatPlainTextParts(raw json.RawMessage) []string { var plain string if json.Unmarshal(raw, &plain) == nil { return []string{plain} } var parts []json.RawMessage if json.Unmarshal(raw, &parts) != nil { return nil } values := make([]string, 0, len(parts)) for _, rawPart := range parts { var part map[string]json.RawMessage if json.Unmarshal(rawPart, &part) != nil { continue } var partType string if json.Unmarshal(part["type"], &partType) != nil { continue } switch partType { case "text", "input_text", "output_text": default: continue } var text string if json.Unmarshal(part["text"], &text) == nil { values = append(values, text) } } return values } 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() }