iop/apps/edge/internal/openai/request_lineage.go

606 lines
22 KiB
Go

package openai
import (
"bytes"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
)
// logicalRequestEndpoint keeps fingerprints from incompatible wire formats
// distinct even when their JSON payloads happen to look alike.
type logicalRequestEndpoint string
const (
logicalRequestEndpointChat logicalRequestEndpoint = "chat_completions"
logicalRequestEndpointAnthropic logicalRequestEndpoint = "anthropic_messages"
)
// logicalRequestLineage is the immutable request prefix and tool contract
// recorded when a logical request is admitted. It intentionally contains only
// digests: raw prompts, tool schemas, and tool results never enter the store.
type logicalRequestLineage struct {
Endpoint logicalRequestEndpoint
HistoryDigest string
ToolsetDigest string
}
type logicalRequestContinuationLineage struct {
Prefix logicalRequestLineage
IssuedCallHash string
ResultIDs []string
Committed logicalRequestLineage
}
func newChatRequestLineage(raw json.RawMessage) (logicalRequestLineage, error) {
fields, err := decodeLogicalRequestLineageEnvelope(raw)
if err != nil {
return logicalRequestLineage{}, err
}
rawMessages, ok := fields["messages"]
if !ok {
return logicalRequestLineage{}, fmt.Errorf("chat messages field is required")
}
if _, err := validateChatMessages(rawMessages); err != nil {
return logicalRequestLineage{}, err
}
return newLogicalRequestLineageFromRawFields(fields, logicalRequestEndpointChat, []string{"model", "messages"})
}
func newAnthropicRequestLineage(raw json.RawMessage) (logicalRequestLineage, error) {
fields, err := decodeLogicalRequestLineageEnvelope(raw)
if err != nil {
return logicalRequestLineage{}, err
}
rawMessages, ok := fields["messages"]
if !ok {
return logicalRequestLineage{}, fmt.Errorf("anthropic messages field is required")
}
if _, err := validateAnthropicMessages(rawMessages); err != nil {
return logicalRequestLineage{}, err
}
return newLogicalRequestLineageFromRawFields(fields, logicalRequestEndpointAnthropic, []string{"model", "system", "messages"})
}
type chatMessageValidation struct {
Role string `json:"role"`
ToolCallID string `json:"tool_call_id"`
ToolCalls []struct {
ID string `json:"id"`
} `json:"tool_calls"`
}
func validateChatMessages(rawMessages json.RawMessage) ([]json.RawMessage, error) {
if len(rawMessages) == 0 {
return nil, fmt.Errorf("chat messages field is required")
}
var msgList []json.RawMessage
decoder := json.NewDecoder(bytes.NewReader(rawMessages))
decoder.UseNumber()
if err := decoder.Decode(&msgList); err != nil {
return nil, fmt.Errorf("chat messages must be an array: %w", err)
}
if len(msgList) == 0 {
return nil, fmt.Errorf("chat messages array must not be empty")
}
validRoles := map[string]struct{}{
"system": {},
"developer": {},
"user": {},
"assistant": {},
"tool": {},
}
globallySeenIssuedIDs := make(map[string]struct{})
pendingToolCallIDs := make(map[string]struct{})
for i, rawMsg := range msgList {
var m chatMessageValidation
if err := json.Unmarshal(rawMsg, &m); err != nil {
return nil, fmt.Errorf("decode chat message at index %d: %w", i, err)
}
if _, ok := validRoles[m.Role]; !ok {
return nil, fmt.Errorf("unknown chat message role %q at index %d", m.Role, i)
}
if m.Role == "tool" {
if len(pendingToolCallIDs) == 0 {
return nil, fmt.Errorf("orphan tool result message at index %d", i)
}
if m.ToolCallID == "" {
return nil, fmt.Errorf("tool message at index %d has empty tool_call_id", i)
}
if _, ok := pendingToolCallIDs[m.ToolCallID]; !ok {
return nil, fmt.Errorf("tool message at index %d has unexpected or duplicate tool_call_id %q", i, m.ToolCallID)
}
delete(pendingToolCallIDs, m.ToolCallID)
} else {
if len(pendingToolCallIDs) > 0 {
return nil, fmt.Errorf("message at index %d with role %q appeared before all preceding tool_calls were satisfied", i, m.Role)
}
if m.Role == "assistant" && len(m.ToolCalls) > 0 {
for tcIdx, tc := range m.ToolCalls {
if tc.ID == "" {
return nil, fmt.Errorf("assistant message at index %d tool call %d has empty id", i, tcIdx)
}
if _, duplicate := globallySeenIssuedIDs[tc.ID]; duplicate {
return nil, fmt.Errorf("duplicate issued assistant tool call id %q at index %d", tc.ID, i)
}
globallySeenIssuedIDs[tc.ID] = struct{}{}
pendingToolCallIDs[tc.ID] = struct{}{}
}
}
}
}
if len(pendingToolCallIDs) > 0 {
return nil, fmt.Errorf("message list ended before all assistant tool_calls were satisfied")
}
return msgList, nil
}
func validateAnthropicMessages(rawMessages json.RawMessage) ([]json.RawMessage, error) {
if len(rawMessages) == 0 {
return nil, fmt.Errorf("anthropic messages field is required")
}
var msgList []json.RawMessage
decoder := json.NewDecoder(bytes.NewReader(rawMessages))
decoder.UseNumber()
if err := decoder.Decode(&msgList); err != nil {
return nil, fmt.Errorf("anthropic messages must be an array: %w", err)
}
if len(msgList) == 0 {
return nil, fmt.Errorf("anthropic messages array must not be empty")
}
globallySeenToolUseIDs := make(map[string]struct{})
pendingToolUseIDs := make(map[string]struct{})
for i, rawMsg := range msgList {
var m struct {
Role string `json:"role"`
Content json.RawMessage `json:"content"`
}
if err := json.Unmarshal(rawMsg, &m); err != nil {
return nil, fmt.Errorf("decode anthropic message at index %d: %w", i, err)
}
if m.Role != "user" && m.Role != "assistant" {
return nil, fmt.Errorf("invalid anthropic message role %q at index %d", m.Role, i)
}
if i == 0 && m.Role != "user" {
return nil, fmt.Errorf("anthropic messages first message must have role user, got %q", m.Role)
}
if i > 0 {
var prev struct {
Role string `json:"role"`
}
_ = json.Unmarshal(msgList[i-1], &prev)
if m.Role == prev.Role {
return nil, fmt.Errorf("anthropic messages roles must alternate, repeated role %q at index %d", m.Role, i)
}
}
blocks, err := decodeAnthropicContent(m.Content)
if err != nil {
return nil, fmt.Errorf("anthropic message %d: %w", i, err)
}
if m.Role == "assistant" {
for bIdx, block := range blocks {
if block.Type == "tool_result" || block.Type == "image" {
return nil, fmt.Errorf("anthropic assistant message %d block %d has invalid type %q", i, bIdx, block.Type)
}
if block.Type == "tool_use" {
if block.ID == "" {
return nil, fmt.Errorf("anthropic assistant message %d tool_use block %d has empty id", i, bIdx)
}
if _, duplicate := globallySeenToolUseIDs[block.ID]; duplicate {
return nil, fmt.Errorf("duplicate issued assistant tool_use id %q at message %d", block.ID, i)
}
globallySeenToolUseIDs[block.ID] = struct{}{}
pendingToolUseIDs[block.ID] = struct{}{}
}
}
} else if m.Role == "user" {
if len(pendingToolUseIDs) > 0 {
if len(blocks) != len(pendingToolUseIDs) {
return nil, fmt.Errorf("anthropic user message %d tool results count (%d) does not match issued tool_use count (%d)", i, len(blocks), len(pendingToolUseIDs))
}
for bIdx, block := range blocks {
if block.Type != "tool_result" {
return nil, fmt.Errorf("anthropic user message %d block %d has non-tool_result type %q when responding to tool_use", i, bIdx, block.Type)
}
if block.ToolUseID == "" {
return nil, fmt.Errorf("anthropic user message %d tool_result block %d missing tool_use_id", i, bIdx)
}
if _, ok := pendingToolUseIDs[block.ToolUseID]; !ok {
return nil, fmt.Errorf("anthropic user message %d tool_result tool_use_id %q not in issued tool_use blocks or duplicate", i, block.ToolUseID)
}
delete(pendingToolUseIDs, block.ToolUseID)
}
} else {
for bIdx, block := range blocks {
if block.Type == "tool_use" || block.Type == "thinking" {
return nil, fmt.Errorf("anthropic user message %d block %d has invalid type %q", i, bIdx, block.Type)
}
if block.Type == "tool_result" {
return nil, fmt.Errorf("orphan tool_result block in anthropic user message at index %d", i)
}
}
}
}
}
if len(pendingToolUseIDs) > 0 {
return nil, fmt.Errorf("anthropic message list ended before tool_use blocks were satisfied")
}
return msgList, nil
}
func newChatContinuationLineage(raw json.RawMessage) (logicalRequestContinuationLineage, error) {
fields, err := decodeLogicalRequestLineageEnvelope(raw)
if err != nil {
return logicalRequestContinuationLineage{}, err
}
rawMessages, ok := fields["messages"]
if !ok {
return logicalRequestContinuationLineage{}, fmt.Errorf("chat continuation messages field is required")
}
msgList, err := validateChatMessages(rawMessages)
if err != nil {
return logicalRequestContinuationLineage{}, err
}
var resultIDs []string
seenResultIDs := make(map[string]struct{})
resultCount := 0
for i := len(msgList) - 1; i >= 0; i-- {
var msg struct {
Role string `json:"role"`
ToolCallID string `json:"tool_call_id"`
}
if err := json.Unmarshal(msgList[i], &msg); err != nil {
return logicalRequestContinuationLineage{}, fmt.Errorf("decode message at index %d: %w", i, err)
}
if msg.Role == "tool" {
if msg.ToolCallID == "" {
return logicalRequestContinuationLineage{}, fmt.Errorf("tool message at index %d has empty tool_call_id", i)
}
if _, exists := seenResultIDs[msg.ToolCallID]; exists {
return logicalRequestContinuationLineage{}, fmt.Errorf("duplicate tool_call_id %q in frontier", msg.ToolCallID)
}
seenResultIDs[msg.ToolCallID] = struct{}{}
resultIDs = append([]string{msg.ToolCallID}, resultIDs...)
resultCount++
} else {
break
}
}
if resultCount == 0 {
return logicalRequestContinuationLineage{}, fmt.Errorf("chat continuation must end with at least one tool result message")
}
assistantIndex := len(msgList) - resultCount - 1
if assistantIndex < 0 {
return logicalRequestContinuationLineage{}, fmt.Errorf("chat continuation missing issued assistant message before tool results")
}
var assistantMsg struct {
Role string `json:"role"`
ToolCalls []struct {
ID string `json:"id"`
} `json:"tool_calls"`
}
if err := json.Unmarshal(msgList[assistantIndex], &assistantMsg); err != nil {
return logicalRequestContinuationLineage{}, fmt.Errorf("decode assistant message: %w", err)
}
if assistantMsg.Role != "assistant" {
return logicalRequestContinuationLineage{}, fmt.Errorf("expected assistant message before tool results, got role %q", assistantMsg.Role)
}
if len(assistantMsg.ToolCalls) == 0 {
return logicalRequestContinuationLineage{}, fmt.Errorf("issued assistant message must contain tool_calls")
}
expectedToolCallIDs := make(map[string]struct{}, len(assistantMsg.ToolCalls))
for _, tc := range assistantMsg.ToolCalls {
if tc.ID == "" {
return logicalRequestContinuationLineage{}, fmt.Errorf("issued assistant tool call has empty id")
}
if _, duplicate := expectedToolCallIDs[tc.ID]; duplicate {
return logicalRequestContinuationLineage{}, fmt.Errorf("duplicate issued assistant tool call id %q", tc.ID)
}
expectedToolCallIDs[tc.ID] = struct{}{}
}
if len(expectedToolCallIDs) != len(seenResultIDs) {
return logicalRequestContinuationLineage{}, fmt.Errorf("frontier tool results count (%d) does not match issued assistant tool_calls count (%d)", len(seenResultIDs), len(expectedToolCallIDs))
}
for id := range seenResultIDs {
if _, ok := expectedToolCallIDs[id]; !ok {
return logicalRequestContinuationLineage{}, fmt.Errorf("frontier tool_call_id %q not in issued assistant tool_calls", id)
}
}
issuedCallHash, err := fingerprintCanonicalJSON(logicalRequestEndpointChat, msgList[assistantIndex])
if err != nil {
return logicalRequestContinuationLineage{}, fmt.Errorf("fingerprint issued assistant call: %w", err)
}
prefixMessagesRaw, err := json.Marshal(msgList[:assistantIndex])
if err != nil {
return logicalRequestContinuationLineage{}, fmt.Errorf("marshal prefix messages: %w", err)
}
prefixHistory := map[string]json.RawMessage{
"model": fields["model"],
"messages": prefixMessagesRaw,
}
prefixHistoryDigest, err := fingerprintCanonicalJSON(logicalRequestEndpointChat, prefixHistory)
if err != nil {
return logicalRequestContinuationLineage{}, err
}
toolsetDigest, err := fingerprintCanonicalJSON(logicalRequestEndpointChat, fields["tools"])
if err != nil {
return logicalRequestContinuationLineage{}, err
}
prefixLineage := logicalRequestLineage{
Endpoint: logicalRequestEndpointChat,
HistoryDigest: prefixHistoryDigest,
ToolsetDigest: toolsetDigest,
}
committedHistory := map[string]json.RawMessage{
"model": fields["model"],
"messages": fields["messages"],
}
committedHistoryDigest, err := fingerprintCanonicalJSON(logicalRequestEndpointChat, committedHistory)
if err != nil {
return logicalRequestContinuationLineage{}, err
}
committedLineage := logicalRequestLineage{
Endpoint: logicalRequestEndpointChat,
HistoryDigest: committedHistoryDigest,
ToolsetDigest: toolsetDigest,
}
return logicalRequestContinuationLineage{
Prefix: prefixLineage,
IssuedCallHash: issuedCallHash,
ResultIDs: resultIDs,
Committed: committedLineage,
}, nil
}
func newAnthropicContinuationLineage(raw json.RawMessage) (logicalRequestContinuationLineage, error) {
fields, err := decodeLogicalRequestLineageEnvelope(raw)
if err != nil {
return logicalRequestContinuationLineage{}, err
}
rawMessages, ok := fields["messages"]
if !ok {
return logicalRequestContinuationLineage{}, fmt.Errorf("anthropic continuation messages field is required")
}
msgList, err := validateAnthropicMessages(rawMessages)
if err != nil {
return logicalRequestContinuationLineage{}, err
}
lastIndex := len(msgList) - 1
var lastMsg struct {
Role string `json:"role"`
Content json.RawMessage `json:"content"`
}
if err := json.Unmarshal(msgList[lastIndex], &lastMsg); err != nil {
return logicalRequestContinuationLineage{}, fmt.Errorf("decode last anthropic message: %w", err)
}
if lastMsg.Role != "user" {
return logicalRequestContinuationLineage{}, fmt.Errorf("anthropic continuation last message must have role user, got %q", lastMsg.Role)
}
var blocks []json.RawMessage
if err := json.Unmarshal(lastMsg.Content, &blocks); err != nil {
return logicalRequestContinuationLineage{}, fmt.Errorf("anthropic continuation user message content must be array of blocks: %w", err)
}
var resultIDs []string
seenResultIDs := make(map[string]struct{})
for bIdx, blockRaw := range blocks {
var block struct {
Type string `json:"type"`
ToolUseID string `json:"tool_use_id"`
}
if err := json.Unmarshal(blockRaw, &block); err != nil {
return logicalRequestContinuationLineage{}, fmt.Errorf("decode content block %d: %w", bIdx, err)
}
if block.Type != "tool_result" {
return logicalRequestContinuationLineage{}, fmt.Errorf("anthropic continuation trailing user message block %d has non-tool_result type %q", bIdx, block.Type)
}
if block.ToolUseID == "" {
return logicalRequestContinuationLineage{}, fmt.Errorf("anthropic tool_result block %d missing tool_use_id", bIdx)
}
if _, exists := seenResultIDs[block.ToolUseID]; exists {
return logicalRequestContinuationLineage{}, fmt.Errorf("duplicate tool_use_id %q in anthropic frontier", block.ToolUseID)
}
seenResultIDs[block.ToolUseID] = struct{}{}
resultIDs = append(resultIDs, block.ToolUseID)
}
if len(resultIDs) == 0 {
return logicalRequestContinuationLineage{}, fmt.Errorf("anthropic continuation trailing user message contains no tool_result blocks")
}
assistantIndex := lastIndex - 1
if assistantIndex < 0 {
return logicalRequestContinuationLineage{}, fmt.Errorf("anthropic continuation missing issued assistant message before tool results")
}
var assistantMsg struct {
Role string `json:"role"`
Content json.RawMessage `json:"content"`
}
if err := json.Unmarshal(msgList[assistantIndex], &assistantMsg); err != nil {
return logicalRequestContinuationLineage{}, fmt.Errorf("decode assistant message: %w", err)
}
if assistantMsg.Role != "assistant" {
return logicalRequestContinuationLineage{}, fmt.Errorf("expected assistant message before tool results, got role %q", assistantMsg.Role)
}
var assistantBlocks []json.RawMessage
if err := json.Unmarshal(assistantMsg.Content, &assistantBlocks); err != nil {
return logicalRequestContinuationLineage{}, fmt.Errorf("assistant message content must be array of blocks: %w", err)
}
expectedToolUseIDs := make(map[string]struct{})
for bIdx, blockRaw := range assistantBlocks {
var block struct {
Type string `json:"type"`
ID string `json:"id"`
}
if err := json.Unmarshal(blockRaw, &block); err != nil {
return logicalRequestContinuationLineage{}, fmt.Errorf("decode assistant content block %d: %w", bIdx, err)
}
if block.Type == "tool_use" {
if block.ID == "" {
return logicalRequestContinuationLineage{}, fmt.Errorf("issued assistant tool_use block has empty id")
}
if _, duplicate := expectedToolUseIDs[block.ID]; duplicate {
return logicalRequestContinuationLineage{}, fmt.Errorf("duplicate issued assistant tool_use id %q", block.ID)
}
expectedToolUseIDs[block.ID] = struct{}{}
}
}
if len(expectedToolUseIDs) == 0 {
return logicalRequestContinuationLineage{}, fmt.Errorf("issued assistant message contains no tool_use blocks")
}
if len(expectedToolUseIDs) != len(seenResultIDs) {
return logicalRequestContinuationLineage{}, fmt.Errorf("anthropic frontier tool results count (%d) does not match issued tool_use count (%d)", len(seenResultIDs), len(expectedToolUseIDs))
}
for id := range seenResultIDs {
if _, ok := expectedToolUseIDs[id]; !ok {
return logicalRequestContinuationLineage{}, fmt.Errorf("anthropic tool_result tool_use_id %q not in issued assistant tool_use blocks", id)
}
}
issuedCallHash, err := fingerprintCanonicalJSON(logicalRequestEndpointAnthropic, msgList[assistantIndex])
if err != nil {
return logicalRequestContinuationLineage{}, fmt.Errorf("fingerprint issued assistant call: %w", err)
}
prefixMessagesRaw, err := json.Marshal(msgList[:assistantIndex])
if err != nil {
return logicalRequestContinuationLineage{}, fmt.Errorf("marshal prefix messages: %w", err)
}
prefixHistory := map[string]json.RawMessage{
"model": fields["model"],
"system": fields["system"],
"messages": prefixMessagesRaw,
}
prefixHistoryDigest, err := fingerprintCanonicalJSON(logicalRequestEndpointAnthropic, prefixHistory)
if err != nil {
return logicalRequestContinuationLineage{}, err
}
toolsetDigest, err := fingerprintCanonicalJSON(logicalRequestEndpointAnthropic, fields["tools"])
if err != nil {
return logicalRequestContinuationLineage{}, err
}
prefixLineage := logicalRequestLineage{
Endpoint: logicalRequestEndpointAnthropic,
HistoryDigest: prefixHistoryDigest,
ToolsetDigest: toolsetDigest,
}
committedHistory := map[string]json.RawMessage{
"model": fields["model"],
"system": fields["system"],
"messages": fields["messages"],
}
committedHistoryDigest, err := fingerprintCanonicalJSON(logicalRequestEndpointAnthropic, committedHistory)
if err != nil {
return logicalRequestContinuationLineage{}, err
}
committedLineage := logicalRequestLineage{
Endpoint: logicalRequestEndpointAnthropic,
HistoryDigest: committedHistoryDigest,
ToolsetDigest: toolsetDigest,
}
return logicalRequestContinuationLineage{
Prefix: prefixLineage,
IssuedCallHash: issuedCallHash,
ResultIDs: resultIDs,
Committed: committedLineage,
}, nil
}
func newLogicalRequestLineageFromRaw(raw json.RawMessage, endpoint logicalRequestEndpoint, historyFields []string) (logicalRequestLineage, error) {
fields, err := decodeLogicalRequestLineageEnvelope(raw)
if err != nil {
return logicalRequestLineage{}, err
}
return newLogicalRequestLineageFromRawFields(fields, endpoint, historyFields)
}
func newLogicalRequestLineageFromRawFields(fields map[string]json.RawMessage, endpoint logicalRequestEndpoint, historyFields []string) (logicalRequestLineage, error) {
history := make(map[string]json.RawMessage, len(historyFields))
for _, field := range historyFields {
history[field] = fields[field]
}
historyDigest, err := fingerprintCanonicalJSON(endpoint, history)
if err != nil {
return logicalRequestLineage{}, err
}
toolsetDigest, err := fingerprintCanonicalJSON(endpoint, fields["tools"])
if err != nil {
return logicalRequestLineage{}, err
}
return logicalRequestLineage{Endpoint: endpoint, HistoryDigest: historyDigest, ToolsetDigest: toolsetDigest}, nil
}
func decodeLogicalRequestLineageEnvelope(raw json.RawMessage) (map[string]json.RawMessage, error) {
decoder := json.NewDecoder(bytes.NewReader(raw))
decoder.UseNumber()
var fields map[string]json.RawMessage
if err := decoder.Decode(&fields); err != nil {
return nil, fmt.Errorf("decode logical request lineage envelope: %w", err)
}
if fields == nil {
return nil, fmt.Errorf("logical request lineage envelope must be an object")
}
var extra any
if err := decoder.Decode(&extra); err == nil {
return nil, fmt.Errorf("logical request lineage envelope contains multiple JSON values")
} else if err != io.EOF {
return nil, fmt.Errorf("decode logical request lineage envelope: %w", err)
}
return fields, nil
}
// fingerprintCanonicalJSON normalizes nested JSON before hashing. Decoding
// RawMessage values first prevents insignificant formatting differences in a
// caller's schema or Anthropic content blocks from becoming new lineages.
func fingerprintCanonicalJSON(endpoint logicalRequestEndpoint, value any) (string, error) {
raw, err := json.Marshal(value)
if err != nil {
return "", fmt.Errorf("marshal logical request lineage: %w", err)
}
var canonical any
decoder := json.NewDecoder(bytes.NewReader(raw))
decoder.UseNumber()
if err := decoder.Decode(&canonical); err != nil {
return "", fmt.Errorf("decode logical request lineage: %w", err)
}
normalized, err := json.Marshal(canonical)
if err != nil {
return "", fmt.Errorf("encode logical request lineage: %w", err)
}
sum := sha256.Sum256(append(append([]byte(endpoint), '\n'), normalized...))
return hex.EncodeToString(sum[:]), nil
}