208 lines
5.1 KiB
Go
208 lines
5.1 KiB
Go
package schemas
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
)
|
|
|
|
// JSONKeyOrder is a lightweight helper that preserves JSON key ordering through
|
|
// struct serialization round-trips. Embed it in any struct with `json:"-"` tag.
|
|
//
|
|
// LLMs are autoregressive sequence models that are sensitive to JSON key ordering
|
|
// in tool schemas. This helper ensures that when Bifrost deserializes and
|
|
// re-serializes JSON, the original key order from the client is preserved.
|
|
//
|
|
// Usage:
|
|
//
|
|
// type MyStruct struct {
|
|
// keyOrder JSONKeyOrder `json:"-"`
|
|
// Field1 string `json:"field1"`
|
|
// Field2 string `json:"field2"`
|
|
// }
|
|
//
|
|
// func (s *MyStruct) UnmarshalJSON(data []byte) error {
|
|
// type Alias MyStruct
|
|
// if err := Unmarshal(data, (*Alias)(s)); err != nil { return err }
|
|
// s.keyOrder.Capture(data)
|
|
// return nil
|
|
// }
|
|
//
|
|
// func (s MyStruct) MarshalJSON() ([]byte, error) {
|
|
// type Alias MyStruct
|
|
// data, err := Marshal(Alias(s))
|
|
// if err != nil { return nil, err }
|
|
// return s.keyOrder.Apply(data)
|
|
// }
|
|
type JSONKeyOrder struct {
|
|
keys []string
|
|
}
|
|
|
|
// Capture extracts and stores the top-level key order from raw JSON data.
|
|
// Call this at the end of UnmarshalJSON.
|
|
func (o *JSONKeyOrder) Capture(data []byte) {
|
|
o.keys = ExtractTopLevelKeyOrder(data)
|
|
}
|
|
|
|
// Apply reorders the keys in serialized JSON to match the captured order.
|
|
// If no order was captured (programmatic construction), returns data unchanged.
|
|
// Call this at the end of MarshalJSON.
|
|
func (o *JSONKeyOrder) Apply(data []byte) ([]byte, error) {
|
|
if len(o.keys) == 0 {
|
|
return data, nil
|
|
}
|
|
return ReorderJSONKeys(data, o.keys)
|
|
}
|
|
|
|
// ExtractTopLevelKeyOrder parses a JSON object and returns its top-level keys in
|
|
// document order. Useful for capturing key order before struct deserialization
|
|
// loses it, so that re-serialization can preserve the original order.
|
|
func ExtractTopLevelKeyOrder(data []byte) []string {
|
|
trimmed := bytes.TrimSpace(data)
|
|
if len(trimmed) == 0 || trimmed[0] != '{' {
|
|
return nil
|
|
}
|
|
|
|
dec := json.NewDecoder(bytes.NewReader(trimmed))
|
|
// Read opening '{'
|
|
if _, err := dec.Token(); err != nil {
|
|
return nil
|
|
}
|
|
|
|
var keys []string
|
|
for dec.More() {
|
|
tok, err := dec.Token()
|
|
if err != nil {
|
|
break
|
|
}
|
|
key, ok := tok.(string)
|
|
if !ok {
|
|
break
|
|
}
|
|
keys = append(keys, key)
|
|
// Skip the value (handles nested objects/arrays)
|
|
if err := skipJSONValue(dec); err != nil {
|
|
break
|
|
}
|
|
}
|
|
return keys
|
|
}
|
|
|
|
// skipJSONValue reads and discards a single JSON value from a decoder.
|
|
func skipJSONValue(dec *json.Decoder) error {
|
|
tok, err := dec.Token()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
delim, ok := tok.(json.Delim)
|
|
if !ok {
|
|
return nil // primitive value, already consumed
|
|
}
|
|
switch delim {
|
|
case '{':
|
|
for dec.More() {
|
|
// skip key
|
|
if _, err := dec.Token(); err != nil {
|
|
return err
|
|
}
|
|
if err := skipJSONValue(dec); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
_, err = dec.Token() // closing '}'
|
|
return err
|
|
case '[':
|
|
for dec.More() {
|
|
if err := skipJSONValue(dec); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
_, err = dec.Token() // closing ']'
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ReorderJSONKeys takes serialized JSON and a desired key order, and returns the
|
|
// same JSON with top-level keys reordered. Keys present in `order` are emitted
|
|
// first in that order; any remaining keys follow in their original order.
|
|
// This is a general-purpose utility for preserving client-specified key order
|
|
// through struct serialization/deserialization round-trips.
|
|
func ReorderJSONKeys(data []byte, order []string) ([]byte, error) {
|
|
// Parse into key → raw value pairs, preserving original values as-is
|
|
trimmed := bytes.TrimSpace(data)
|
|
if len(trimmed) < 2 || trimmed[0] != '{' {
|
|
return data, nil
|
|
}
|
|
|
|
// Use encoding/json decoder to get raw key-value pairs while preserving order
|
|
dec := json.NewDecoder(bytes.NewReader(trimmed))
|
|
dec.UseNumber()
|
|
if _, err := dec.Token(); err != nil { // '{'
|
|
return data, nil
|
|
}
|
|
|
|
type kvPair struct {
|
|
key string
|
|
val json.RawMessage
|
|
}
|
|
|
|
var pairs []kvPair
|
|
pairMap := make(map[string]json.RawMessage)
|
|
for dec.More() {
|
|
tok, err := dec.Token()
|
|
if err != nil {
|
|
return data, nil
|
|
}
|
|
key, ok := tok.(string)
|
|
if !ok {
|
|
return data, nil
|
|
}
|
|
var val json.RawMessage
|
|
if err := dec.Decode(&val); err != nil {
|
|
return data, nil
|
|
}
|
|
pairs = append(pairs, kvPair{key, val})
|
|
pairMap[key] = val
|
|
}
|
|
|
|
// Rebuild JSON: first keys from `order`, then remaining keys in original order
|
|
var buf bytes.Buffer
|
|
buf.WriteByte('{')
|
|
first := true
|
|
emitted := make(map[string]bool, len(order))
|
|
|
|
for _, key := range order {
|
|
val, exists := pairMap[key]
|
|
if !exists {
|
|
continue
|
|
}
|
|
if !first {
|
|
buf.WriteByte(',')
|
|
}
|
|
first = false
|
|
keyBytes, _ := MarshalSorted(key)
|
|
buf.Write(keyBytes)
|
|
buf.WriteByte(':')
|
|
buf.Write(val)
|
|
emitted[key] = true
|
|
}
|
|
|
|
// Remaining keys in their original document order
|
|
for _, kv := range pairs {
|
|
if emitted[kv.key] {
|
|
continue
|
|
}
|
|
if !first {
|
|
buf.WriteByte(',')
|
|
}
|
|
first = false
|
|
keyBytes, _ := MarshalSorted(kv.key)
|
|
buf.Write(keyBytes)
|
|
buf.WriteByte(':')
|
|
buf.Write(kv.val)
|
|
}
|
|
|
|
buf.WriteByte('}')
|
|
return buf.Bytes(), nil
|
|
}
|