vibe-proxy/backend/internal/translator/claude/interactions/interactions_claude_response.go
2026-08-24 00:10:41 +02:00

595 lines
21 KiB
Go

package interactions
import (
"bytes"
"context"
"fmt"
"strings"
"time"
translatorcommon "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/common"
"github.com/tidwall/gjson"
"github.com/tidwall/sjson"
)
var claudeInteractionsDataTag = []byte("data:")
type claudeToInteractionsStreamState struct {
ID string
Model string
Created bool
StatusUpdated bool
Completed bool
Done bool
UsageRaw []byte
StepIndex int
ActiveStepIndex int
ActiveStepType string
ActiveStepOpen bool
CurrentStepByIndex map[int]string
ToolNames map[int]string
ToolIDs map[int]string
ToolArgs map[int]*strings.Builder
}
func ConvertClaudeResponseToInteractions(ctx context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) [][]byte {
_ = ctx
_ = originalRequestRawJSON
_ = requestRawJSON
if param == nil {
var local any
param = &local
}
if *param == nil {
*param = &claudeToInteractionsStreamState{Model: modelName}
}
st := (*param).(*claudeToInteractionsStreamState)
st.Model = firstNonEmptyString(st.Model, modelName)
st.ensureMaps()
return convertClaudeEventToInteractions(modelName, rawJSON, st)
}
func ConvertClaudeResponseToInteractionsNonStream(ctx context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) []byte {
_ = ctx
_ = originalRequestRawJSON
_ = requestRawJSON
root := gjson.ParseBytes(rawJSON)
if root.Exists() && root.Get("content").Exists() {
return convertClaudeMessageToInteractions(modelName, root)
}
return convertClaudeSSEToInteractionsNonStream(modelName, rawJSON)
}
func convertClaudeMessageToInteractions(modelName string, root gjson.Result) []byte {
out := []byte(`{"id":"","object":"interaction","status":"completed","model":"","steps":[]}`)
out, _ = sjson.SetBytes(out, "id", firstNonEmptyString(root.Get("id").String(), fmt.Sprintf("interaction_%d", time.Now().UnixNano())))
out, _ = sjson.SetBytes(out, "model", firstNonEmptyString(root.Get("model").String(), modelName))
steps := make([][]byte, 0, 4)
root.Get("content").ForEach(func(_, part gjson.Result) bool {
if step := claudeContentBlockToInteractionsStep(part); len(step) > 0 {
steps = append(steps, step)
}
return true
})
if len(steps) > 0 {
out, _ = sjson.SetRawBytes(out, "steps", translatorcommon.JoinRawArray(steps))
}
out = setInteractionsUsageFromClaude(out, "usage", root.Get("usage"))
return out
}
func convertClaudeSSEToInteractionsNonStream(modelName string, rawJSON []byte) []byte {
out := []byte(`{"id":"","object":"interaction","status":"completed","model":"","steps":[]}`)
out, _ = sjson.SetBytes(out, "id", fmt.Sprintf("interaction_%d", time.Now().UnixNano()))
out, _ = sjson.SetBytes(out, "model", modelName)
st := &claudeToInteractionsStreamState{Model: modelName}
st.ensureMaps()
steps := make([][]byte, 0, 8)
remaining := rawJSON
for len(remaining) > 0 {
var line []byte
idx := bytes.IndexByte(remaining, '\n')
if idx >= 0 {
line = remaining[:idx]
remaining = remaining[idx+1:]
} else {
line = remaining
remaining = nil
}
line = bytes.TrimSpace(line)
if !bytes.HasPrefix(line, claudeInteractionsDataTag) {
continue
}
payload := bytes.TrimSpace(line[len(claudeInteractionsDataTag):])
if bytes.Equal(payload, []byte("[DONE]")) {
continue
}
root := gjson.ParseBytes(payload)
switch root.Get("type").String() {
case "message_start":
msg := root.Get("message")
if id := msg.Get("id").String(); id != "" {
out, _ = sjson.SetBytes(out, "id", id)
}
if model := msg.Get("model").String(); model != "" {
out, _ = sjson.SetBytes(out, "model", model)
}
mergeClaudeUsage(st, msg.Get("usage"))
case "content_block_start":
claudeNonStreamContentBlockStart(root, st)
case "content_block_delta":
claudeNonStreamContentBlockDelta(root, st)
case "content_block_stop":
if step := claudeNonStreamContentBlockStop(root, st); len(step) > 0 {
steps = append(steps, step)
}
case "message_delta":
mergeClaudeUsage(st, root.Get("usage"))
}
}
if len(steps) > 0 {
out, _ = sjson.SetRawBytes(out, "steps", translatorcommon.JoinRawArray(steps))
}
out = setInteractionsUsageFromClaude(out, "usage", claudeMergedUsage(st))
return out
}
func convertClaudeEventToInteractions(modelName string, rawJSON []byte, st *claudeToInteractionsStreamState) [][]byte {
payload := claudeInteractionsSSEPayload(rawJSON)
if len(payload) == 0 {
return nil
}
if bytes.Equal(bytes.TrimSpace(payload), []byte("[DONE]")) {
return appendClaudeInteractionsDone(nil, st)
}
root := gjson.ParseBytes(payload)
switch root.Get("type").String() {
case "message_start":
msg := root.Get("message")
st.ID = firstNonEmptyString(msg.Get("id").String(), st.ID, fmt.Sprintf("interaction_%d", time.Now().UnixNano()))
st.Model = firstNonEmptyString(msg.Get("model").String(), st.Model, modelName)
mergeClaudeUsage(st, msg.Get("usage"))
return appendClaudeInteractionsCreated(nil, st, st.Model)
case "content_block_start":
return claudeContentBlockStartToInteractions(modelName, root, st)
case "content_block_delta":
return claudeContentBlockDeltaToInteractions(modelName, root, st)
case "content_block_stop":
return claudeContentBlockStopToInteractions(root, st)
case "message_delta":
mergeClaudeUsage(st, root.Get("usage"))
out := appendClaudeInteractionsStepStop(nil, st)
out = appendClaudeInteractionsCompleted(out, st, modelName, root)
return out
case "message_stop":
if st.Completed {
return nil
}
return appendClaudeInteractionsCompleted(nil, st, modelName, root)
case "error":
out := appendClaudeInteractionsCreated(nil, st, modelName)
return appendClaudeInteractionsCompleted(out, st, modelName, root)
}
return nil
}
func claudeContentBlockStartToInteractions(modelName string, root gjson.Result, st *claudeToInteractionsStreamState) [][]byte {
out := appendClaudeInteractionsCreated(nil, st, modelName)
out = appendClaudeInteractionsStepStop(out, st)
index := int(root.Get("index").Int())
block := root.Get("content_block")
stepType := claudeBlockInteractionsStepType(block.Get("type").String())
st.CurrentStepByIndex[index] = stepType
if stepType == "function_call" {
if name := block.Get("name").String(); name != "" {
st.ToolNames[index] = name
}
if id := block.Get("id").String(); id != "" {
st.ToolIDs[index] = id
}
if input := block.Get("input"); input.Exists() && input.IsObject() && input.Raw != "{}" {
builder := &strings.Builder{}
builder.WriteString(input.Raw)
st.ToolArgs[index] = builder
}
}
step := claudeBlockToInteractionsStep(block, stepType)
return appendClaudeInteractionsStepStart(out, st, stepType, step)
}
func claudeContentBlockDeltaToInteractions(modelName string, root gjson.Result, st *claudeToInteractionsStreamState) [][]byte {
index := int(root.Get("index").Int())
stepType := st.CurrentStepByIndex[index]
if stepType == "" {
stepType = claudeDeltaInteractionsStepType(root.Get("delta.type").String())
out := appendClaudeInteractionsCreated(nil, st, modelName)
out = appendClaudeInteractionsStepStop(out, st)
out = appendClaudeInteractionsStepStart(out, st, stepType, []byte(`{"type":"`+stepType+`"}`))
st.CurrentStepByIndex[index] = stepType
return appendClaudeDeltaToInteractions(out, st, root.Get("delta"), index)
}
if !st.ActiveStepOpen || st.ActiveStepIndex != index {
out := appendClaudeInteractionsCreated(nil, st, modelName)
out = appendClaudeInteractionsStepStop(out, st)
step := claudeStepForKnownIndex(stepType, index, st)
out = appendClaudeInteractionsStepStart(out, st, stepType, step)
return appendClaudeDeltaToInteractions(out, st, root.Get("delta"), index)
}
return appendClaudeDeltaToInteractions(nil, st, root.Get("delta"), index)
}
func claudeContentBlockStopToInteractions(root gjson.Result, st *claudeToInteractionsStreamState) [][]byte {
index := int(root.Get("index").Int())
out := appendClaudeInteractionsStepStop(nil, st)
delete(st.CurrentStepByIndex, index)
delete(st.ToolNames, index)
delete(st.ToolIDs, index)
delete(st.ToolArgs, index)
return out
}
func appendClaudeDeltaToInteractions(out [][]byte, st *claudeToInteractionsStreamState, delta gjson.Result, index int) [][]byte {
switch delta.Get("type").String() {
case "text_delta":
return appendClaudeInteractionsTextDelta(out, st, delta.Get("text").String(), false)
case "thinking_delta":
return appendClaudeInteractionsTextDelta(out, st, delta.Get("thinking").String(), true)
case "input_json_delta":
if st.ToolArgs[index] == nil {
st.ToolArgs[index] = &strings.Builder{}
}
partial := delta.Get("partial_json").String()
st.ToolArgs[index].WriteString(partial)
return appendClaudeInteractionsArgumentsDelta(out, st, partial)
}
return out
}
func claudeContentBlockToInteractionsStep(part gjson.Result) []byte {
switch part.Get("type").String() {
case "text":
step := []byte(`{"type":"model_output","content":[]}`)
content := []byte(`{"type":"text","text":""}`)
content, _ = sjson.SetBytes(content, "text", part.Get("text").String())
return translatorcommon.SetRawArrayItems(step, "content", [][]byte{content})
case "thinking":
step := []byte(`{"type":"thought","content":[]}`)
content := []byte(`{"type":"text","text":""}`)
content, _ = sjson.SetBytes(content, "text", part.Get("thinking").String())
return translatorcommon.SetRawArrayItems(step, "content", [][]byte{content})
case "tool_use":
return claudeToolUseToInteractionsStep(part, strings.TrimSpace(part.Get("input").Raw))
}
return nil
}
func claudeToolUseToInteractionsStep(part gjson.Result, argsRaw string) []byte {
step := []byte(`{"type":"function_call","name":"","arguments":{}}`)
step, _ = sjson.SetBytes(step, "name", part.Get("name").String())
if id := part.Get("id").String(); id != "" {
step, _ = sjson.SetBytes(step, "id", id)
step, _ = sjson.SetBytes(step, "call_id", id)
}
if argsRaw != "" && gjson.Valid(argsRaw) {
step, _ = sjson.SetRawBytes(step, "arguments", []byte(argsRaw))
}
return step
}
func claudeBlockToInteractionsStep(block gjson.Result, stepType string) []byte {
step := []byte(`{"type":""}`)
step, _ = sjson.SetBytes(step, "type", stepType)
if stepType == "function_call" {
step, _ = sjson.SetBytes(step, "name", block.Get("name").String())
if id := block.Get("id").String(); id != "" {
step, _ = sjson.SetBytes(step, "id", id)
step, _ = sjson.SetBytes(step, "call_id", id)
}
step, _ = sjson.SetRawBytes(step, "arguments", []byte(`{}`))
}
return step
}
func claudeStepForKnownIndex(stepType string, index int, st *claudeToInteractionsStreamState) []byte {
step := []byte(`{"type":""}`)
step, _ = sjson.SetBytes(step, "type", stepType)
if stepType == "function_call" {
step, _ = sjson.SetBytes(step, "name", st.ToolNames[index])
if id := st.ToolIDs[index]; id != "" {
step, _ = sjson.SetBytes(step, "id", id)
step, _ = sjson.SetBytes(step, "call_id", id)
}
step, _ = sjson.SetRawBytes(step, "arguments", []byte(`{}`))
}
return step
}
func claudeNonStreamContentBlockStart(root gjson.Result, st *claudeToInteractionsStreamState) {
index := int(root.Get("index").Int())
block := root.Get("content_block")
st.CurrentStepByIndex[index] = claudeBlockInteractionsStepType(block.Get("type").String())
if block.Get("type").String() != "tool_use" {
return
}
st.ToolNames[index] = block.Get("name").String()
st.ToolIDs[index] = block.Get("id").String()
if input := block.Get("input"); input.Exists() && input.IsObject() && input.Raw != "{}" {
builder := &strings.Builder{}
builder.WriteString(input.Raw)
st.ToolArgs[index] = builder
}
}
func claudeNonStreamContentBlockDelta(root gjson.Result, st *claudeToInteractionsStreamState) {
index := int(root.Get("index").Int())
delta := root.Get("delta")
switch delta.Get("type").String() {
case "text_delta", "thinking_delta":
if st.ToolArgs[index] == nil {
st.ToolArgs[index] = &strings.Builder{}
}
if delta.Get("type").String() == "text_delta" {
st.ToolArgs[index].WriteString(delta.Get("text").String())
} else {
st.ToolArgs[index].WriteString(delta.Get("thinking").String())
}
case "input_json_delta":
if st.ToolArgs[index] == nil {
st.ToolArgs[index] = &strings.Builder{}
}
st.ToolArgs[index].WriteString(delta.Get("partial_json").String())
}
}
func claudeNonStreamContentBlockStop(root gjson.Result, st *claudeToInteractionsStreamState) []byte {
index := int(root.Get("index").Int())
stepType := st.CurrentStepByIndex[index]
builder := st.ToolArgs[index]
text := ""
if builder != nil {
text = builder.String()
}
var step []byte
switch stepType {
case "thought":
step = []byte(`{"type":"thought","content":[]}`)
content := []byte(`{"type":"text","text":""}`)
content, _ = sjson.SetBytes(content, "text", text)
step = translatorcommon.SetRawArrayItems(step, "content", [][]byte{content})
case "function_call":
part := []byte(`{"type":"tool_use","id":"","name":"","input":{}}`)
part, _ = sjson.SetBytes(part, "id", st.ToolIDs[index])
part, _ = sjson.SetBytes(part, "name", st.ToolNames[index])
step = claudeToolUseToInteractionsStep(gjson.ParseBytes(part), strings.TrimSpace(text))
default:
step = []byte(`{"type":"model_output","content":[]}`)
content := []byte(`{"type":"text","text":""}`)
content, _ = sjson.SetBytes(content, "text", text)
step = translatorcommon.SetRawArrayItems(step, "content", [][]byte{content})
}
delete(st.CurrentStepByIndex, index)
delete(st.ToolNames, index)
delete(st.ToolIDs, index)
delete(st.ToolArgs, index)
return step
}
func mergeClaudeUsage(st *claudeToInteractionsStreamState, usage gjson.Result) {
if !usage.Exists() {
return
}
if len(st.UsageRaw) == 0 {
st.UsageRaw = []byte(`{}`)
}
for _, key := range []string{
"input_tokens",
"output_tokens",
"cache_read_input_tokens",
"cache_creation_input_tokens",
"thinking_tokens",
} {
value := usage.Get(key)
if !value.Exists() {
continue
}
st.UsageRaw, _ = sjson.SetRawBytes(st.UsageRaw, key, []byte(value.Raw))
}
}
func claudeMergedUsage(st *claudeToInteractionsStreamState) gjson.Result {
if len(st.UsageRaw) == 0 {
return gjson.Result{}
}
return gjson.ParseBytes(st.UsageRaw)
}
func setInteractionsUsageFromClaude(out []byte, path string, usage gjson.Result) []byte {
if !usage.Exists() {
return out
}
inputTokens := usage.Get("input_tokens").Int()
outputTokens := usage.Get("output_tokens").Int()
cacheRead := usage.Get("cache_read_input_tokens").Int()
cacheCreation := usage.Get("cache_creation_input_tokens").Int()
thinkingTokens := usage.Get("thinking_tokens").Int()
if usage.Get("input_tokens").Exists() {
out, _ = sjson.SetBytes(out, path+".input_tokens", inputTokens)
out, _ = sjson.SetBytes(out, path+".total_input_tokens", inputTokens)
}
if usage.Get("output_tokens").Exists() {
out, _ = sjson.SetBytes(out, path+".output_tokens", outputTokens)
out, _ = sjson.SetBytes(out, path+".total_output_tokens", outputTokens)
}
total := inputTokens + outputTokens
if usage.Get("input_tokens").Exists() || usage.Get("output_tokens").Exists() {
out, _ = sjson.SetBytes(out, path+".total_tokens", total)
}
if cacheRead != 0 || cacheCreation != 0 {
out, _ = sjson.SetBytes(out, path+".cached_tokens", cacheRead+cacheCreation)
out, _ = sjson.SetBytes(out, path+".total_cached_tokens", cacheRead+cacheCreation)
}
if thinkingTokens != 0 {
out, _ = sjson.SetBytes(out, path+".reasoning_tokens", thinkingTokens)
out, _ = sjson.SetBytes(out, path+".total_thought_tokens", thinkingTokens)
}
return out
}
func appendClaudeInteractionsCreated(out [][]byte, st *claudeToInteractionsStreamState, modelName string) [][]byte {
if st.Created {
return out
}
st.ID = firstNonEmptyString(st.ID, fmt.Sprintf("interaction_%d", time.Now().UnixNano()))
created := []byte(`{"interaction":{"id":"","status":"in_progress","object":"interaction","model":""},"event_type":"interaction.created"}`)
created, _ = sjson.SetBytes(created, "interaction.id", st.ID)
created, _ = sjson.SetBytes(created, "interaction.model", firstNonEmptyString(st.Model, modelName))
out = append(out, translatorcommon.SSEEventData("interaction.created", created))
st.Created = true
return appendClaudeInteractionsStatusUpdate(out, st)
}
func appendClaudeInteractionsStatusUpdate(out [][]byte, st *claudeToInteractionsStreamState) [][]byte {
if st.StatusUpdated {
return out
}
statusUpdate := []byte(`{"interaction_id":"","status":"in_progress","event_type":"interaction.status_update"}`)
statusUpdate, _ = sjson.SetBytes(statusUpdate, "interaction_id", st.ID)
out = append(out, translatorcommon.SSEEventData("interaction.status_update", statusUpdate))
st.StatusUpdated = true
return out
}
func appendClaudeInteractionsStepStart(out [][]byte, st *claudeToInteractionsStreamState, stepType string, step []byte) [][]byte {
st.ActiveStepIndex = st.StepIndex
st.ActiveStepType = stepType
st.ActiveStepOpen = true
payload := []byte(`{"index":0,"step":{"type":""},"event_type":"step.start"}`)
payload, _ = sjson.SetBytes(payload, "index", st.ActiveStepIndex)
if len(step) > 0 && gjson.ValidBytes(step) {
payload, _ = sjson.SetRawBytes(payload, "step", step)
} else {
payload, _ = sjson.SetBytes(payload, "step.type", stepType)
}
return append(out, translatorcommon.SSEEventData("step.start", payload))
}
func appendClaudeInteractionsTextDelta(out [][]byte, st *claudeToInteractionsStreamState, text string, thought bool) [][]byte {
payload := []byte(`{"index":0,"delta":{"text":"","type":"text"},"event_type":"step.delta"}`)
payload, _ = sjson.SetBytes(payload, "index", st.ActiveStepIndex)
if thought {
payload, _ = sjson.SetBytes(payload, "delta.type", "thought_summary")
payload, _ = sjson.SetBytes(payload, "delta.content.type", "text")
payload, _ = sjson.SetBytes(payload, "delta.content.text", text)
payload, _ = sjson.DeleteBytes(payload, "delta.text")
} else {
payload, _ = sjson.SetBytes(payload, "delta.text", text)
}
return append(out, translatorcommon.SSEEventData("step.delta", payload))
}
func appendClaudeInteractionsArgumentsDelta(out [][]byte, st *claudeToInteractionsStreamState, arguments string) [][]byte {
payload := []byte(`{"index":0,"delta":{"arguments":"","type":"arguments_delta"},"event_type":"step.delta"}`)
payload, _ = sjson.SetBytes(payload, "index", st.ActiveStepIndex)
payload, _ = sjson.SetBytes(payload, "delta.arguments", arguments)
return append(out, translatorcommon.SSEEventData("step.delta", payload))
}
func appendClaudeInteractionsStepStop(out [][]byte, st *claudeToInteractionsStreamState) [][]byte {
if !st.ActiveStepOpen {
return out
}
payload := []byte(`{"index":0,"event_type":"step.stop"}`)
payload, _ = sjson.SetBytes(payload, "index", st.ActiveStepIndex)
out = append(out, translatorcommon.SSEEventData("step.stop", payload))
st.ActiveStepOpen = false
st.ActiveStepType = ""
st.StepIndex++
return out
}
func appendClaudeInteractionsCompleted(out [][]byte, st *claudeToInteractionsStreamState, modelName string, root gjson.Result) [][]byte {
if st.Completed {
return out
}
out = appendClaudeInteractionsCreated(out, st, modelName)
now := time.Now().UTC().Format(time.RFC3339)
completed := []byte(`{"interaction":{"id":"","status":"completed","usage":{},"created":"","updated":"","service_tier":"standard","object":"interaction","model":""},"event_type":"interaction.completed"}`)
completed, _ = sjson.SetBytes(completed, "interaction.id", st.ID)
completed, _ = sjson.SetBytes(completed, "interaction.created", now)
completed, _ = sjson.SetBytes(completed, "interaction.updated", now)
completed, _ = sjson.SetBytes(completed, "interaction.model", firstNonEmptyString(st.Model, modelName))
usage := claudeMergedUsage(st)
if !usage.Exists() {
usage = root.Get("usage")
}
completed = setInteractionsUsageFromClaude(completed, "interaction.usage", usage)
out = append(out, translatorcommon.SSEEventData("interaction.completed", completed))
st.Completed = true
return out
}
func appendClaudeInteractionsDone(out [][]byte, st *claudeToInteractionsStreamState) [][]byte {
if st.Done {
return out
}
out = append(out, translatorcommon.SSEEventData("done", []byte("[DONE]")))
st.Done = true
return out
}
func claudeInteractionsSSEPayload(rawJSON []byte) []byte {
rawJSON = bytes.TrimSpace(rawJSON)
if bytes.Equal(rawJSON, []byte("[DONE]")) {
return rawJSON
}
if !bytes.HasPrefix(rawJSON, claudeInteractionsDataTag) {
return nil
}
return bytes.TrimSpace(rawJSON[len(claudeInteractionsDataTag):])
}
func claudeBlockInteractionsStepType(blockType string) string {
switch blockType {
case "thinking":
return "thought"
case "tool_use":
return "function_call"
default:
return "model_output"
}
}
func claudeDeltaInteractionsStepType(deltaType string) string {
switch deltaType {
case "thinking_delta":
return "thought"
case "input_json_delta":
return "function_call"
default:
return "model_output"
}
}
func (st *claudeToInteractionsStreamState) ensureMaps() {
if st.CurrentStepByIndex == nil {
st.CurrentStepByIndex = make(map[int]string)
}
if st.ToolNames == nil {
st.ToolNames = make(map[int]string)
}
if st.ToolIDs == nil {
st.ToolIDs = make(map[int]string)
}
if st.ToolArgs == nil {
st.ToolArgs = make(map[int]*strings.Builder)
}
}
func firstNonEmptyString(values ...string) string {
for _, value := range values {
if value != "" {
return value
}
}
return ""
}