Files
CLIProxyAPI/internal/runtime/executor/helps/devin_payload.go
Luis Pater 868e137f6c feat(payload): introduce payload finalizer for protobuf and multipart requests
- Add `PayloadFinalizer` to apply configuration rules at the final serialization barrier
- Support decoding, modifying, and re-encoding Devin protobuf business fields
- Add multipart form-data payload normalization to JSON for media payload rules

Closes: #5067
2026-10-04 03:01:34 +08:00

228 lines
7.6 KiB
Go

package helps
import (
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"math"
"github.com/tidwall/gjson"
"github.com/tidwall/sjson"
"google.golang.org/protobuf/encoding/protowire"
)
// Devin payload rules address the native business fields, never credentials or
// Connect framing. Encoding after the barrier does not inject defaults or sanitize.
type devinPayloadField struct {
name string
kind protowire.Type
repeated bool
binary bool
jsonValue bool
children map[protowire.Number]devinPayloadField
}
var devinPayloadFields = map[protowire.Number]devinPayloadField{
2: {name: "system_prompt", kind: protowire.BytesType},
3: {name: "prompts", kind: protowire.BytesType, repeated: true, children: map[protowire.Number]devinPayloadField{
1: {name: "id", kind: protowire.BytesType},
2: {name: "source", kind: protowire.VarintType},
3: {name: "content", kind: protowire.BytesType},
6: {name: "tool_calls", kind: protowire.BytesType, repeated: true, children: map[protowire.Number]devinPayloadField{
1: {name: "id", kind: protowire.BytesType},
2: {name: "name", kind: protowire.BytesType},
3: {name: "arguments", kind: protowire.BytesType},
}},
7: {name: "tool_call_id", kind: protowire.BytesType},
10: {name: "images", kind: protowire.BytesType, repeated: true, children: map[protowire.Number]devinPayloadField{
1: {name: "data", kind: protowire.BytesType},
2: {name: "mime_type", kind: protowire.BytesType},
}},
11: {name: "thinking", kind: protowire.BytesType},
12: {name: "signature", kind: protowire.BytesType, binary: true},
18: {name: "signature_type", kind: protowire.BytesType},
}},
8: {name: "completion_config", kind: protowire.BytesType, children: map[protowire.Number]devinPayloadField{
1: {name: "enabled", kind: protowire.VarintType},
2: {name: "max_tokens", kind: protowire.VarintType},
3: {name: "parameter_3", kind: protowire.VarintType},
5: {name: "temperature", kind: protowire.Fixed64Type},
7: {name: "top_k", kind: protowire.VarintType},
8: {name: "top_p", kind: protowire.Fixed64Type},
}},
10: {name: "tools", kind: protowire.BytesType, repeated: true, children: map[protowire.Number]devinPayloadField{
1: {name: "name", kind: protowire.BytesType},
2: {name: "description", kind: protowire.BytesType},
3: {name: "parameters", kind: protowire.BytesType, jsonValue: true},
}},
16: {name: "cascade_id", kind: protowire.BytesType},
21: {name: "model", kind: protowire.BytesType},
}
// DevinPayloadDefaultsSource maps caller field presence to the native business
// view so defaults can replace built-in values without replacing caller values.
func DevinPayloadDefaultsSource(native, interactions []byte) []byte {
out := []byte(`{}`)
for target, sources := range map[string][]string{
"model": {"model"},
"system_prompt": {"system_instruction", "systemInstruction"},
"prompts": {"input"},
"tools": {"tools"},
"cascade_id": {"session_id", "sessionId", "conversation_id", "previous_interaction_id"},
"completion_config.max_tokens": {"generation_config.max_output_tokens", "generationConfig.max_output_tokens"},
"completion_config.temperature": {"generation_config.temperature", "generationConfig.temperature", "temperature"},
"completion_config.top_k": {"generation_config.top_k", "generationConfig.top_k"},
"completion_config.top_p": {"generation_config.top_p", "generationConfig.top_p"},
} {
var value gjson.Result
for _, source := range sources {
value = gjson.GetBytes(interactions, source)
if value.Exists() {
break
}
}
if !value.Exists() {
continue
}
if target == "prompts" || target == "tools" {
value = gjson.GetBytes(native, target)
}
if value.Exists() {
out, _ = sjson.SetRawBytes(out, target, []byte(value.Raw))
}
}
return out
}
// FinalizeDevinPayload projects the already-normalized protobuf business fields
// to JSON for user rules, then replaces only those fields in the wire message.
func FinalizeDevinPayload(wire []byte, finalize PayloadFinalizer) ([]byte, []byte, error) {
view, errDecode := decodeDevinPayload(wire, devinPayloadFields)
if errDecode != nil {
return nil, nil, errDecode
}
body, errMarshal := json.Marshal(view)
if errMarshal != nil {
return nil, nil, errMarshal
}
configured := finalize(body)
if bytes.Equal(configured, body) {
return wire, configured, nil
}
body = configured
out, errEncode := encodeDevinPayload(wire, gjson.ParseBytes(body), devinPayloadFields)
return out, body, errEncode
}
func decodeDevinPayload(wire []byte, fields map[protowire.Number]devinPayloadField) (map[string]any, error) {
out := make(map[string]any)
for len(wire) > 0 {
num, kind, tagLen := protowire.ConsumeTag(wire)
if tagLen < 0 {
return nil, protowire.ParseError(tagLen)
}
wire = wire[tagLen:]
n := protowire.ConsumeFieldValue(num, kind, wire)
if n < 0 {
return nil, protowire.ParseError(n)
}
field, ok := fields[num]
if ok {
var value any
switch kind {
case protowire.BytesType:
data, _ := protowire.ConsumeBytes(wire)
switch {
case field.children != nil:
decoded, errDecode := decodeDevinPayload(data, field.children)
if errDecode != nil {
return nil, errDecode
}
value = decoded
case field.binary:
value = base64.StdEncoding.EncodeToString(data)
case field.jsonValue && json.Valid(data):
value = json.RawMessage(data)
default:
value = string(data)
}
case protowire.VarintType:
value, _ = protowire.ConsumeVarint(wire)
case protowire.Fixed64Type:
bits, _ := protowire.ConsumeFixed64(wire)
value = math.Float64frombits(bits)
}
if field.repeated {
values, _ := out[field.name].([]any)
out[field.name] = append(values, value)
} else {
out[field.name] = value
}
}
wire = wire[n:]
}
return out, nil
}
func encodeDevinPayload(original []byte, body gjson.Result, fields map[protowire.Number]devinPayloadField) ([]byte, error) {
var out []byte
// Credentials, device metadata, thread ordinals, and protocol flags stay opaque.
for len(original) > 0 {
num, kind, tagLen := protowire.ConsumeTag(original)
if tagLen < 0 {
return nil, protowire.ParseError(tagLen)
}
n := protowire.ConsumeFieldValue(num, kind, original[tagLen:])
if n < 0 {
return nil, protowire.ParseError(n)
}
if _, ok := fields[num]; !ok {
out = append(out, original[:tagLen+n]...)
}
original = original[tagLen+n:]
}
for num := protowire.Number(1); num <= 21; num++ {
field, ok := fields[num]
if !ok {
continue
}
value := body.Get(field.name)
if !value.Exists() || value.Type == gjson.Null {
continue
}
values := []gjson.Result{value}
if field.repeated {
values = value.Array()
}
for _, item := range values {
out = protowire.AppendTag(out, num, field.kind)
switch field.kind {
case protowire.BytesType:
data := []byte(item.String())
if field.children != nil {
var errEncode error
data, errEncode = encodeDevinPayload(nil, item, field.children)
if errEncode != nil {
return nil, errEncode
}
} else if field.binary {
var errDecode error
data, errDecode = base64.StdEncoding.DecodeString(item.String())
if errDecode != nil {
return nil, fmt.Errorf("Devin payload %s: %w", field.name, errDecode)
}
} else if field.jsonValue {
data = []byte(item.Raw)
}
out = protowire.AppendBytes(out, data)
case protowire.VarintType:
out = protowire.AppendVarint(out, item.Uint())
case protowire.Fixed64Type:
out = protowire.AppendFixed64(out, math.Float64bits(item.Float()))
}
}
}
return out, nil
}