595 lines
21 KiB
Go
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 ""
|
|
}
|