package openai import ( "encoding/json" "errors" "fmt" "strings" "iop/packages/go/streamgate" ) func decodeResponsesRequest(dec *json.Decoder, req *responsesRequest) 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", "input", "instructions", "stream", "background", "metadata", "max_output_tokens", "temperature", "top_p": default: return fmt.Errorf("%s is not supported for /v1/responses", 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/responses request format") } if req.MaxOutputTokens != nil && *req.MaxOutputTokens <= 0 { return fmt.Errorf("max_output_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") } return nil } // decodeOpenAIResponsesRepeatHistory is intentionally independent from the // Chat messages decoder. It reads only plain Responses input provenance and // reduces it to bounded fingerprints; encrypted and unknown input items remain // canonical-only data and never become mutation candidates. func decodeOpenAIResponsesRepeatHistory(rawBody []byte) (openAIRepeatHistorySnapshot, error) { var root struct { Input json.RawMessage `json:"input"` } if err := json.Unmarshal(rawBody, &root); err != nil { return openAIRepeatHistorySnapshot{}, fmt.Errorf("decode Responses repeat history: %w", err) } builder := newOpenAIRepeatHistoryBuilder() var inputString string if json.Unmarshal(root.Input, &inputString) == nil { builder.recordText("user", openAIRepeatChannelContent, inputString) return builder.snapshot(), nil } var items []json.RawMessage if json.Unmarshal(root.Input, &items) != nil { return builder.snapshot(), nil } pendingActions := make(map[string]streamgate.FixedFingerprint) for _, rawItem := range items { var item map[string]json.RawMessage if json.Unmarshal(rawItem, &item) != nil { continue } var itemType string if json.Unmarshal(item["type"], &itemType) != nil { continue } switch itemType { case "message": var role string if json.Unmarshal(item["role"], &role) != nil { continue } for _, text := range openAIRepeatResponsesTextParts(item["content"], "input_text", "output_text", "text") { builder.recordText(role, openAIRepeatChannelContent, text) } case "reasoning": for _, text := range openAIRepeatResponsesTextParts(item["content"], "reasoning_text") { builder.recordText("assistant", openAIRepeatChannelReasoning, text) } case "function_call": var callID, name string var arguments json.RawMessage _ = json.Unmarshal(item["call_id"], &callID) if callID == "" { _ = json.Unmarshal(item["id"], &callID) } _ = json.Unmarshal(item["name"], &name) arguments = openAIRepeatArgumentsValue(item["arguments"]) if fingerprint, ok := openAIRepeatActionFingerprint(name, arguments); ok && strings.TrimSpace(callID) != "" { pendingActions[callID] = fingerprint } case "function_call_output", "function_call_error": var callID string _ = json.Unmarshal(item["call_id"], &callID) action, ok := pendingActions[callID] if !ok { continue } var result string if json.Unmarshal(item["output"], &result) != nil { _ = json.Unmarshal(item["error"], &result) } if fingerprint, ok := openAIRepeatTextFingerprint(result); ok { builder.recordCompletedAction(action, fingerprint) } } } return builder.snapshot(), nil } func openAIRepeatResponsesTextParts(raw json.RawMessage, allowedTypes ...string) []string { allowed := make(map[string]struct{}, len(allowedTypes)) for _, value := range allowedTypes { allowed[value] = struct{}{} } 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, text string if json.Unmarshal(part["type"], &partType) != nil { continue } if _, ok := allowed[partType]; !ok { continue } if json.Unmarshal(part["text"], &text) == nil { values = append(values, text) } } return values } // decodeOpenAIResponsesResumeRequest accepts only the private recovery body // emitted by buildOpenAIResponsesResumeBody. It must never be used for public // /v1/responses ingress: public arrays remain rejected by parseResponsesInput. // // The decoder is intentionally exact. It admits only a non-streaming model, // the fixed resume directive, and either an assistant output item or a // reasoning item immediately followed by that assistant output item. It does // not trim, normalize, or reinterpret recorded text. func decodeOpenAIResponsesResumeRequest(body []byte) (responsesResumeRequest, error) { var raw map[string]json.RawMessage if err := json.Unmarshal(body, &raw); err != nil { return responsesResumeRequest{}, fmt.Errorf("invalid OpenAI Responses resume request") } if len(raw) < 4 || len(raw) > 5 { return responsesResumeRequest{}, fmt.Errorf("invalid OpenAI Responses resume request") } for key := range raw { switch key { case "model", "stream", "instructions", "input", "temperature": default: return responsesResumeRequest{}, fmt.Errorf("invalid OpenAI Responses resume request") } } var model string if err := json.Unmarshal(raw["model"], &model); err != nil || strings.TrimSpace(model) == "" { return responsesResumeRequest{}, fmt.Errorf("invalid OpenAI Responses resume request") } var stream bool if err := json.Unmarshal(raw["stream"], &stream); err != nil || stream { return responsesResumeRequest{}, fmt.Errorf("invalid OpenAI Responses resume request") } var instructions string if err := json.Unmarshal(raw["instructions"], &instructions); err != nil || instructions != openAIRepeatResumeDirective { return responsesResumeRequest{}, fmt.Errorf("invalid OpenAI Responses resume request") } var temperature *float64 if rawTemperature, ok := raw["temperature"]; ok { var value float64 if err := json.Unmarshal(rawTemperature, &value); err != nil || value < 0 || value > 2 { return responsesResumeRequest{}, fmt.Errorf("invalid OpenAI Responses resume request") } temperature = &value } var items []json.RawMessage if err := json.Unmarshal(raw["input"], &items); err != nil || len(items) < 1 || len(items) > 2 { return responsesResumeRequest{}, fmt.Errorf("invalid OpenAI Responses resume request") } decodeTextItem := func(rawItem json.RawMessage, wantType string) (string, error) { var item map[string]json.RawMessage if err := json.Unmarshal(rawItem, &item); err != nil { return "", err } wantItemFields := 2 if wantType == "message" { wantItemFields = 3 } if len(item) != wantItemFields { return "", fmt.Errorf("unexpected item fields") } var itemType string if err := json.Unmarshal(item["type"], &itemType); err != nil || itemType != wantType { return "", fmt.Errorf("unexpected item type") } if wantType == "message" { var role string if err := json.Unmarshal(item["role"], &role); err != nil || role != "assistant" { return "", fmt.Errorf("unexpected item role") } } var content []json.RawMessage if err := json.Unmarshal(item["content"], &content); err != nil || len(content) != 1 { return "", fmt.Errorf("invalid item content") } var part map[string]json.RawMessage if err := json.Unmarshal(content[0], &part); err != nil { return "", err } if len(part) != 2 { return "", fmt.Errorf("unexpected content fields") } partType := "output_text" if wantType == "reasoning" { partType = "reasoning_text" } var gotPartType, text string if err := json.Unmarshal(part["type"], &gotPartType); err != nil || gotPartType != partType { return "", fmt.Errorf("unexpected content type") } if err := json.Unmarshal(part["text"], &text); err != nil { return "", err } return text, nil } var content, reasoning string if len(items) == 2 { var err error reasoning, err = decodeTextItem(items[0], "reasoning") if err != nil { return responsesResumeRequest{}, fmt.Errorf("invalid OpenAI Responses resume request") } content, err = decodeTextItem(items[1], "message") if err != nil { return responsesResumeRequest{}, fmt.Errorf("invalid OpenAI Responses resume request") } } else { var err error content, err = decodeTextItem(items[0], "message") if err != nil { return responsesResumeRequest{}, fmt.Errorf("invalid OpenAI Responses resume request") } } return responsesResumeRequest{ Request: responsesRequest{ Model: model, Input: raw["input"], Instructions: instructions, Stream: false, Temperature: temperature, }, Content: content, Reasoning: reasoning, }, nil } // decodeResponsesEnvelope leniently extracts only the routing-relevant fields // (model, metadata, stream, background) from a /v1/responses request body. It // does not reject unknown fields so the provider tunnel passthrough can forward // Codex/Responses payloads verbatim; strict field validation stays on the // normalized non-provider path. func decodeResponsesEnvelope(rawBody []byte) (responsesEnvelope, error) { var env responsesEnvelope if err := json.Unmarshal(rawBody, &env); err != nil { return responsesEnvelope{}, fmt.Errorf("invalid JSON request") } return env, nil } func parseResponsesInput(raw json.RawMessage) (string, error) { if len(raw) == 0 { return "", fmt.Errorf("input is required") } var s string if err := json.Unmarshal(raw, &s); err != nil { return "", fmt.Errorf("input must be a string") } if strings.TrimSpace(s) == "" { return "", fmt.Errorf("input is required") } return s, nil } func buildResponsesPrompt(instructions, input string) string { instructions = strings.TrimSpace(instructions) if instructions == "" { return input } return instructions + "\n\n" + input } func parseOpenAIMetadata(raw json.RawMessage) (map[string]string, string, error) { if len(raw) == 0 || string(raw) == "null" { return make(map[string]string), "", nil } var rawMap map[string]json.RawMessage if err := json.Unmarshal(raw, &rawMap); err != nil { return nil, "", fmt.Errorf("metadata must be an object") } if len(rawMap) > 16 { return nil, "", fmt.Errorf("metadata must contain at most 16 keys") } flat := make(map[string]string, len(rawMap)) var workspace string for key, rawValue := range rawMap { if len(key) > 64 { return nil, "", fmt.Errorf("metadata key %q exceeds 64 characters", key) } if key == "source" { return nil, "", fmt.Errorf("metadata.source is not supported") } if key == "workspace" { workspaceValue, err := metadataStringValue(key, rawValue) if err != nil { return nil, "", err } workspace = strings.TrimSpace(workspaceValue) continue } value, err := metadataStringValue(key, rawValue) if err != nil { return nil, "", err } flat[key] = value } return flat, workspace, nil } // errProviderRequestValidation is a sentinel for client request validation // failures that occur inside the normalized provider-pool PrepareRun hook // (strict decode, stream/background rejection, input parsing). These errors // are reported as HTTP 400 invalid_request_error, distinct from backend // dispatch errors that return 502. var errProviderRequestValidation = errors.New("provider_request_validation") // isValidationError reports whether err originated from a PrepareRun validation // failure in the normalized provider-pool path. func isValidationError(err error) bool { return errors.Is(err, errProviderRequestValidation) } // metadataStringValue extracts a single string value from a json.RawMessage // for the metadata field. It rejects non-string values and enforces the // 512-character length cap. func metadataStringValue(key string, raw json.RawMessage) (string, error) { var value string if err := json.Unmarshal(raw, &value); err != nil { return "", fmt.Errorf("metadata.%s must be a string", key) } if len(value) > 512 { return "", fmt.Errorf("metadata.%s exceeds 512 characters", key) } return value, nil }