fix(openai): canonicalize issued tool calls
This commit is contained in:
parent
51e5e4f35b
commit
fe08764fcf
4 changed files with 52 additions and 14 deletions
|
|
@ -739,6 +739,17 @@ func (s *artifactFrontierStore) matchRecordLocked(
|
|||
return record, true, nil
|
||||
}
|
||||
}
|
||||
for _, record := range candidates {
|
||||
if record.ownerEdgeID != ownerEdgeID || record.principalRef != principalRef || record.protocol != protocol {
|
||||
continue
|
||||
}
|
||||
if record.lineage != lineage.Prefix {
|
||||
return nil, true, describeArtifactPrefixMismatch(record.lineage, lineage.Prefix)
|
||||
}
|
||||
if record.pendingHash != lineage.IssuedCallHash {
|
||||
return nil, true, fmt.Errorf("%w: issued tool calls changed", errLogicalRequestLineage)
|
||||
}
|
||||
}
|
||||
return nil, true, errLogicalRequestLineage
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -389,7 +389,7 @@ func TestIssuedToolHashIgnoresSDKAssistantDecoration(t *testing.T) {
|
|||
"content":null,
|
||||
"reasoning_content":"sdk-normalized",
|
||||
"provider_metadata":{"ignored":true},
|
||||
"tool_calls":[{"id":"call_1","type":"function","function":{"name":"bash","arguments":"{\"command\":\"printf ok\"}"}}]
|
||||
"tool_calls":[{"id":"call_1","type":"function","provider_metadata":{"ignored":true},"function":{"name":"bash","arguments":"{ \"command\" : \"printf ok\" }"}}]
|
||||
}`))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
|
|
|||
|
|
@ -106,7 +106,7 @@ func TestPresetRequestIdentityAcrossChatTurns(t *testing.T) {
|
|||
|
||||
// Simulate stage 1 assistant issuing tool call "call_c1"
|
||||
assistantMsg := json.RawMessage(`{"role":"assistant","tool_calls":[{"id":"call_c1","type":"function","function":{"name":"search"}}]}`)
|
||||
issuedHash, err := fingerprintCanonicalJSON(logicalRequestEndpointChat, assistantMsg)
|
||||
issuedHash, err := chatIssuedCallHash(assistantMsg)
|
||||
if err != nil {
|
||||
t.Fatalf("fingerprintCanonicalJSON: %v", err)
|
||||
}
|
||||
|
|
@ -249,7 +249,7 @@ func TestPresetRequestIdentityAcrossAnthropicTurns(t *testing.T) {
|
|||
|
||||
// Simulate assistant issuing tool_use block tu_a1
|
||||
assistantMsg := json.RawMessage(`{"role":"assistant","content":[{"type":"tool_use","id":"tu_a1","name":"search","input":{}}]}`)
|
||||
issuedHash, err := fingerprintCanonicalJSON(logicalRequestEndpointAnthropic, assistantMsg)
|
||||
issuedHash, err := anthropicIssuedCallHash(assistantMsg)
|
||||
if err != nil {
|
||||
t.Fatalf("fingerprintCanonicalJSON: %v", err)
|
||||
}
|
||||
|
|
@ -362,7 +362,7 @@ func TestPresetRequestIdentityRejectionCases(t *testing.T) {
|
|||
coord.mu.Unlock()
|
||||
|
||||
assistantMsg := json.RawMessage(`{"role":"assistant","tool_calls":[{"id":"call_r1","type":"function","function":{"name":"search"}}]}`)
|
||||
issuedHash, err := fingerprintCanonicalJSON(logicalRequestEndpointChat, assistantMsg)
|
||||
issuedHash, err := chatIssuedCallHash(assistantMsg)
|
||||
if err != nil {
|
||||
t.Fatalf("fingerprintCanonicalJSON: %v", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import (
|
|||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// logicalRequestEndpoint keeps fingerprints from incompatible wire formats
|
||||
|
|
@ -575,14 +576,36 @@ func newAnthropicContinuationLineage(raw json.RawMessage) (logicalRequestContinu
|
|||
|
||||
func chatIssuedCallHash(raw json.RawMessage) (string, error) {
|
||||
var message struct {
|
||||
Role string `json:"role"`
|
||||
ToolCalls []json.RawMessage `json:"tool_calls"`
|
||||
Role string `json:"role"`
|
||||
ToolCalls []struct {
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Function struct {
|
||||
Name string `json:"name"`
|
||||
Arguments string `json:"arguments"`
|
||||
} `json:"function"`
|
||||
} `json:"tool_calls"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &message); err != nil {
|
||||
return "", err
|
||||
}
|
||||
semanticCalls := make([]map[string]any, 0, len(message.ToolCalls))
|
||||
for _, call := range message.ToolCalls {
|
||||
var arguments any = map[string]any{}
|
||||
if strings.TrimSpace(call.Function.Arguments) != "" {
|
||||
decoder := json.NewDecoder(bytes.NewBufferString(call.Function.Arguments))
|
||||
decoder.UseNumber()
|
||||
if err := decoder.Decode(&arguments); err != nil {
|
||||
return "", fmt.Errorf("decode issued tool arguments: %w", err)
|
||||
}
|
||||
}
|
||||
semanticCalls = append(semanticCalls, map[string]any{
|
||||
"id": call.ID, "type": call.Type,
|
||||
"function": map[string]any{"name": call.Function.Name, "arguments": arguments},
|
||||
})
|
||||
}
|
||||
return fingerprintCanonicalJSON(logicalRequestEndpointChat, map[string]any{
|
||||
"role": message.Role, "tool_calls": message.ToolCalls,
|
||||
"role": message.Role, "tool_calls": semanticCalls,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -594,16 +617,20 @@ func anthropicIssuedCallHash(raw json.RawMessage) (string, error) {
|
|||
if err := json.Unmarshal(raw, &message); err != nil {
|
||||
return "", err
|
||||
}
|
||||
toolUses := make([]json.RawMessage, 0, len(message.Content))
|
||||
type semanticToolUse struct {
|
||||
Type string `json:"type"`
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Input json.RawMessage `json:"input"`
|
||||
}
|
||||
toolUses := make([]semanticToolUse, 0, len(message.Content))
|
||||
for _, block := range message.Content {
|
||||
var header struct {
|
||||
Type string `json:"type"`
|
||||
}
|
||||
if err := json.Unmarshal(block, &header); err != nil {
|
||||
var toolUse semanticToolUse
|
||||
if err := json.Unmarshal(block, &toolUse); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if header.Type == "tool_use" {
|
||||
toolUses = append(toolUses, block)
|
||||
if toolUse.Type == "tool_use" {
|
||||
toolUses = append(toolUses, toolUse)
|
||||
}
|
||||
}
|
||||
return fingerprintCanonicalJSON(logicalRequestEndpointAnthropic, map[string]any{
|
||||
|
|
|
|||
Loading…
Reference in a new issue