Add projects
This commit is contained in:
parent
2d3a9ad623
commit
8b607dd700
1802 changed files with 503346 additions and 2 deletions
19
backend/internal/translator/claude/interactions/init.go
Normal file
19
backend/internal/translator/claude/interactions/init.go
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
package interactions
|
||||
|
||||
import (
|
||||
. "github.com/router-for-me/CLIProxyAPI/v7/internal/constant"
|
||||
"github.com/router-for-me/CLIProxyAPI/v7/internal/interfaces"
|
||||
"github.com/router-for-me/CLIProxyAPI/v7/internal/translator/translator"
|
||||
)
|
||||
|
||||
func init() {
|
||||
translator.Register(
|
||||
Interactions,
|
||||
Claude,
|
||||
ConvertInteractionsRequestToClaude,
|
||||
interfaces.TranslateResponse{
|
||||
Stream: ConvertClaudeResponseToInteractions,
|
||||
NonStream: ConvertClaudeResponseToInteractionsNonStream,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,461 @@
|
|||
package interactions
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/router-for-me/CLIProxyAPI/v7/internal/thinking"
|
||||
translatorcommon "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/common"
|
||||
"github.com/router-for-me/CLIProxyAPI/v7/internal/util"
|
||||
"github.com/tidwall/gjson"
|
||||
"github.com/tidwall/sjson"
|
||||
)
|
||||
|
||||
func ConvertInteractionsRequestToClaude(modelName string, inputRawJSON []byte, stream bool) []byte {
|
||||
root := gjson.ParseBytes(inputRawJSON)
|
||||
out := []byte(`{"model":"","max_tokens":32000,"messages":[]}`)
|
||||
out, _ = sjson.SetBytes(out, "model", modelName)
|
||||
if stream || root.Get("stream").Bool() {
|
||||
out, _ = sjson.SetBytes(out, "stream", true)
|
||||
}
|
||||
out = copyInteractionsSystemToClaude(out, root)
|
||||
out = copyInteractionsGenerationConfigToClaude(out, root)
|
||||
messageAccumulator := translatorcommon.NewClaudeMessageAccumulator(int(root.Get("input.#").Int()))
|
||||
appendInteractionsInputToClaudeMessages(messageAccumulator, root.Get("input"))
|
||||
out = translatorcommon.SetRawArrayItems(out, "messages", messageAccumulator.Messages())
|
||||
out = copyInteractionsToolsToClaude(out, root)
|
||||
return out
|
||||
}
|
||||
|
||||
func copyInteractionsSystemToClaude(out []byte, root gjson.Result) []byte {
|
||||
sys := root.Get("system_instruction")
|
||||
if !sys.Exists() {
|
||||
sys = root.Get("systemInstruction")
|
||||
}
|
||||
text := interactionsClaudeText(sys)
|
||||
if text == "" {
|
||||
return out
|
||||
}
|
||||
out, _ = sjson.SetBytes(out, "system", text)
|
||||
return out
|
||||
}
|
||||
|
||||
func copyInteractionsGenerationConfigToClaude(out []byte, root gjson.Result) []byte {
|
||||
cfg := root.Get("generation_config")
|
||||
if !cfg.Exists() {
|
||||
cfg = root.Get("generationConfig")
|
||||
}
|
||||
if cfg.Exists() {
|
||||
out = copyJSONField(out, cfg, "max_output_tokens", "max_tokens")
|
||||
out = copyJSONField(out, cfg, "maxOutputTokens", "max_tokens")
|
||||
out = copyJSONField(out, cfg, "top_p", "top_p")
|
||||
out = copyJSONField(out, cfg, "topP", "top_p")
|
||||
out = copyJSONField(out, cfg, "temperature", "temperature")
|
||||
out = copyJSONField(out, cfg, "stop_sequences", "stop_sequences")
|
||||
out = copyJSONField(out, cfg, "stopSequences", "stop_sequences")
|
||||
out = copyInteractionsThinkingConfigToClaude(out, cfg)
|
||||
out = copyInteractionsToolChoiceToClaude(out, cfg.Get("tool_choice"))
|
||||
out = copyInteractionsToolChoiceToClaude(out, cfg.Get("toolChoice"))
|
||||
}
|
||||
out = copyInteractionsReasoningToClaude(out, root.Get("reasoning"))
|
||||
out = copyInteractionsToolChoiceToClaude(out, root.Get("tool_choice"))
|
||||
out = copyInteractionsToolChoiceToClaude(out, root.Get("toolChoice"))
|
||||
return out
|
||||
}
|
||||
|
||||
func copyJSONField(out []byte, root gjson.Result, from, to string) []byte {
|
||||
value := root.Get(from)
|
||||
if !value.Exists() {
|
||||
return out
|
||||
}
|
||||
out, _ = sjson.SetRawBytes(out, to, []byte(value.Raw))
|
||||
return out
|
||||
}
|
||||
|
||||
func copyInteractionsThinkingConfigToClaude(out []byte, cfg gjson.Result) []byte {
|
||||
level := firstClaudeInteractionsExisting(cfg, "thinking_level", "thinkingLevel", "reasoning.effort")
|
||||
if !level.Exists() {
|
||||
return out
|
||||
}
|
||||
return setClaudeThinkingFromLevel(out, level.String())
|
||||
}
|
||||
|
||||
func copyInteractionsReasoningToClaude(out []byte, reasoning gjson.Result) []byte {
|
||||
if !reasoning.Exists() {
|
||||
return out
|
||||
}
|
||||
if effort := reasoning.Get("effort"); effort.Exists() {
|
||||
return setClaudeThinkingFromLevel(out, effort.String())
|
||||
}
|
||||
if level := reasoning.Get("thinking_level"); level.Exists() {
|
||||
return setClaudeThinkingFromLevel(out, level.String())
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func setClaudeThinkingFromLevel(out []byte, level string) []byte {
|
||||
normalized := strings.ToLower(strings.TrimSpace(level))
|
||||
if normalized == "" {
|
||||
return out
|
||||
}
|
||||
switch normalized {
|
||||
case "none", "disabled", "off", "false":
|
||||
out, _ = sjson.SetBytes(out, "thinking.type", "disabled")
|
||||
out, _ = sjson.DeleteBytes(out, "thinking.budget_tokens")
|
||||
return out
|
||||
case "auto", "adaptive":
|
||||
out, _ = sjson.SetBytes(out, "thinking.type", "adaptive")
|
||||
out, _ = sjson.DeleteBytes(out, "thinking.budget_tokens")
|
||||
return out
|
||||
}
|
||||
if budget, ok := thinking.ConvertLevelToBudget(normalized); ok {
|
||||
switch {
|
||||
case budget == 0:
|
||||
out, _ = sjson.SetBytes(out, "thinking.type", "disabled")
|
||||
case budget < 0:
|
||||
out, _ = sjson.SetBytes(out, "thinking.type", "enabled")
|
||||
default:
|
||||
out, _ = sjson.SetBytes(out, "thinking.type", "enabled")
|
||||
out, _ = sjson.SetBytes(out, "thinking.budget_tokens", budget)
|
||||
}
|
||||
return out
|
||||
}
|
||||
out, _ = sjson.SetBytes(out, "thinking.type", "adaptive")
|
||||
out, _ = sjson.SetBytes(out, "output_config.effort", normalized)
|
||||
return out
|
||||
}
|
||||
|
||||
func appendInteractionsInputToClaudeMessages(accumulator *translatorcommon.ClaudeMessageAccumulator, input gjson.Result) {
|
||||
if !input.Exists() {
|
||||
return
|
||||
}
|
||||
if input.Type == gjson.String {
|
||||
step := []byte(`{"type":"user_input","content":[{"type":"text","text":""}]}`)
|
||||
step, _ = sjson.SetBytes(step, "content.0.text", input.String())
|
||||
appendInteractionsStepToClaude(accumulator, gjson.ParseBytes(step), "user")
|
||||
return
|
||||
}
|
||||
if input.IsObject() {
|
||||
appendInteractionsInputItemToClaude(accumulator, input)
|
||||
return
|
||||
}
|
||||
input.ForEach(func(_, step gjson.Result) bool {
|
||||
appendInteractionsInputItemToClaude(accumulator, step)
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
func appendInteractionsInputItemToClaude(accumulator *translatorcommon.ClaudeMessageAccumulator, step gjson.Result) {
|
||||
if step.Get("steps").IsArray() {
|
||||
defaultRole := "user"
|
||||
if role := step.Get("role").String(); role == "model" || role == "assistant" {
|
||||
defaultRole = "assistant"
|
||||
}
|
||||
step.Get("steps").ForEach(func(_, nestedStep gjson.Result) bool {
|
||||
appendInteractionsStepToClaude(accumulator, nestedStep, defaultRole)
|
||||
return true
|
||||
})
|
||||
return
|
||||
}
|
||||
if step.Get("parts").Exists() {
|
||||
wrapped := []byte(`{"type":"user_input","content":[]}`)
|
||||
if role := step.Get("role").String(); role == "model" || role == "assistant" {
|
||||
wrapped, _ = sjson.SetBytes(wrapped, "type", "model_output")
|
||||
}
|
||||
wrapped, _ = sjson.SetRawBytes(wrapped, "content", []byte(step.Get("parts").Raw))
|
||||
appendInteractionsStepToClaude(accumulator, gjson.ParseBytes(wrapped), "user")
|
||||
return
|
||||
}
|
||||
stepType := step.Get("type").String()
|
||||
switch stepType {
|
||||
case "function_call":
|
||||
appendInteractionsFunctionCallToClaude(accumulator, step)
|
||||
case "function_result":
|
||||
appendInteractionsFunctionResultToClaude(accumulator, step)
|
||||
case "model_output", "thought":
|
||||
appendInteractionsStepToClaude(accumulator, step, "assistant")
|
||||
default:
|
||||
appendInteractionsStepToClaude(accumulator, step, "user")
|
||||
}
|
||||
}
|
||||
|
||||
func appendInteractionsStepToClaude(accumulator *translatorcommon.ClaudeMessageAccumulator, step gjson.Result, defaultRole string) {
|
||||
role := defaultRole
|
||||
if stepRole := step.Get("role").String(); stepRole == "user" || stepRole == "assistant" {
|
||||
role = stepRole
|
||||
}
|
||||
contentItems := make([][]byte, 0, 4)
|
||||
stepContent := step.Get("content")
|
||||
if stepContent.Type == gjson.String {
|
||||
part := []byte(`{"type":"text","text":""}`)
|
||||
part, _ = sjson.SetBytes(part, "text", stepContent.String())
|
||||
contentItems = append(contentItems, part)
|
||||
} else if stepContent.IsArray() {
|
||||
stepContent.ForEach(func(_, part gjson.Result) bool {
|
||||
if converted := interactionsContentToClaude(part, role); len(converted) > 0 {
|
||||
contentItems = append(contentItems, converted)
|
||||
}
|
||||
return true
|
||||
})
|
||||
} else if text := step.Get("text"); text.Exists() {
|
||||
part := []byte(`{"type":"text","text":""}`)
|
||||
part, _ = sjson.SetBytes(part, "text", text.String())
|
||||
contentItems = append(contentItems, part)
|
||||
}
|
||||
if len(contentItems) == 0 {
|
||||
return
|
||||
}
|
||||
msg := []byte(`{"role":"","content":[]}`)
|
||||
msg, _ = sjson.SetBytes(msg, "role", role)
|
||||
msg, _ = sjson.SetRawBytes(msg, "content", translatorcommon.JoinRawArray(contentItems))
|
||||
accumulator.Append(msg)
|
||||
}
|
||||
|
||||
func interactionsContentToClaude(part gjson.Result, role string) []byte {
|
||||
partType := part.Get("type").String()
|
||||
if partType == "" && part.Get("text").Exists() {
|
||||
partType = "text"
|
||||
}
|
||||
switch partType {
|
||||
case "text":
|
||||
textPart := []byte(`{"type":"text","text":""}`)
|
||||
textPart, _ = sjson.SetBytes(textPart, "text", part.Get("text").String())
|
||||
return textPart
|
||||
case "thinking", "reasoning":
|
||||
if role != "assistant" {
|
||||
return nil
|
||||
}
|
||||
thinkingPart := []byte(`{"type":"thinking","thinking":""}`)
|
||||
thinkingPart, _ = sjson.SetBytes(thinkingPart, "thinking", interactionsClaudeText(part))
|
||||
return thinkingPart
|
||||
case "image":
|
||||
imagePart, _ := interactionsClaudeMediaPart(part, "image")
|
||||
return imagePart
|
||||
case "document", "file":
|
||||
documentPart, _ := interactionsClaudeMediaPart(part, "document")
|
||||
return documentPart
|
||||
default:
|
||||
if text := interactionsClaudeText(part); text != "" {
|
||||
textPart := []byte(`{"type":"text","text":""}`)
|
||||
textPart, _ = sjson.SetBytes(textPart, "text", text)
|
||||
return textPart
|
||||
}
|
||||
if part.Get("data").String() != "" || part.Get("file_data").String() != "" {
|
||||
textPart := []byte(`{"type":"text","text":""}`)
|
||||
textPart, _ = sjson.SetBytes(textPart, "text", fmt.Sprintf("[%s content omitted]", partType))
|
||||
return textPart
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func appendInteractionsFunctionCallToClaude(accumulator *translatorcommon.ClaudeMessageAccumulator, step gjson.Result) {
|
||||
toolUse := []byte(`{"type":"tool_use","id":"","name":"","input":{}}`)
|
||||
toolUse, _ = sjson.SetBytes(toolUse, "id", interactionsClaudeToolID(step))
|
||||
toolUse, _ = sjson.SetBytes(toolUse, "name", step.Get("name").String())
|
||||
args := step.Get("arguments")
|
||||
if !args.Exists() {
|
||||
args = step.Get("args")
|
||||
}
|
||||
if args.Exists() && args.IsObject() {
|
||||
toolUse, _ = sjson.SetRawBytes(toolUse, "input", []byte(args.Raw))
|
||||
}
|
||||
msg := []byte(`{"role":"assistant","content":[]}`)
|
||||
msg, _ = sjson.SetRawBytes(msg, "content", translatorcommon.JoinRawArray([][]byte{toolUse}))
|
||||
accumulator.Append(msg)
|
||||
}
|
||||
|
||||
func appendInteractionsFunctionResultToClaude(accumulator *translatorcommon.ClaudeMessageAccumulator, step gjson.Result) {
|
||||
toolResult := []byte(`{"type":"tool_result","tool_use_id":"","content":""}`)
|
||||
toolResult, _ = sjson.SetBytes(toolResult, "tool_use_id", interactionsClaudeToolID(step))
|
||||
result := step.Get("result")
|
||||
if !result.Exists() {
|
||||
result = step.Get("output")
|
||||
}
|
||||
switch {
|
||||
case result.IsArray():
|
||||
contentItems := make([][]byte, 0, 4)
|
||||
result.ForEach(func(_, part gjson.Result) bool {
|
||||
if converted := interactionsContentToClaude(part, "user"); len(converted) > 0 {
|
||||
contentItems = append(contentItems, converted)
|
||||
}
|
||||
return true
|
||||
})
|
||||
toolResult, _ = sjson.SetRawBytes(toolResult, "content", translatorcommon.JoinRawArray(contentItems))
|
||||
case result.Exists() && result.Raw != "":
|
||||
toolResult, _ = sjson.SetBytes(toolResult, "content", result.Raw)
|
||||
default:
|
||||
toolResult, _ = sjson.SetBytes(toolResult, "content", "")
|
||||
}
|
||||
msg := []byte(`{"role":"user","content":[]}`)
|
||||
msg, _ = sjson.SetRawBytes(msg, "content", translatorcommon.JoinRawArray([][]byte{toolResult}))
|
||||
accumulator.Append(msg)
|
||||
}
|
||||
|
||||
func copyInteractionsToolsToClaude(out []byte, root gjson.Result) []byte {
|
||||
tools := root.Get("tools")
|
||||
if !tools.Exists() || !tools.IsArray() {
|
||||
return out
|
||||
}
|
||||
var toolItems [][]byte
|
||||
tools.ForEach(func(_, tool gjson.Result) bool {
|
||||
if tool.Get("function_declarations").IsArray() {
|
||||
tool.Get("function_declarations").ForEach(func(_, decl gjson.Result) bool {
|
||||
if converted := interactionsClaudeTool(decl); len(converted) > 0 {
|
||||
toolItems = append(toolItems, converted)
|
||||
}
|
||||
return true
|
||||
})
|
||||
return true
|
||||
}
|
||||
if tool.Get("functionDeclarations").IsArray() {
|
||||
tool.Get("functionDeclarations").ForEach(func(_, decl gjson.Result) bool {
|
||||
if converted := interactionsClaudeTool(decl); len(converted) > 0 {
|
||||
toolItems = append(toolItems, converted)
|
||||
}
|
||||
return true
|
||||
})
|
||||
return true
|
||||
}
|
||||
if converted := interactionsClaudeTool(tool); len(converted) > 0 {
|
||||
toolItems = append(toolItems, converted)
|
||||
}
|
||||
return true
|
||||
})
|
||||
if len(toolItems) > 0 {
|
||||
out, _ = sjson.SetRawBytes(out, "tools", translatorcommon.JoinRawArray(toolItems))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func interactionsClaudeTool(tool gjson.Result) []byte {
|
||||
name := tool.Get("name").String()
|
||||
if name == "" {
|
||||
name = tool.Get("function.name").String()
|
||||
}
|
||||
if name == "" {
|
||||
return nil
|
||||
}
|
||||
converted := []byte(`{"name":"","input_schema":{}}`)
|
||||
converted, _ = sjson.SetBytes(converted, "name", name)
|
||||
if desc := tool.Get("description"); desc.Exists() {
|
||||
converted, _ = sjson.SetBytes(converted, "description", desc.String())
|
||||
} else if desc := tool.Get("function.description"); desc.Exists() {
|
||||
converted, _ = sjson.SetBytes(converted, "description", desc.String())
|
||||
}
|
||||
params := firstClaudeInteractionsExisting(tool, "parameters", "parametersJsonSchema", "parameters_json_schema", "input_schema")
|
||||
if params.Exists() && params.IsObject() {
|
||||
converted, _ = sjson.SetRawBytes(converted, "input_schema", []byte(params.Raw))
|
||||
}
|
||||
return converted
|
||||
}
|
||||
|
||||
func copyInteractionsToolChoiceToClaude(out []byte, toolChoice gjson.Result) []byte {
|
||||
if !toolChoice.Exists() {
|
||||
return out
|
||||
}
|
||||
switch toolChoice.Type {
|
||||
case gjson.String:
|
||||
switch strings.ToLower(strings.TrimSpace(toolChoice.String())) {
|
||||
case "auto":
|
||||
out, _ = sjson.SetRawBytes(out, "tool_choice", []byte(`{"type":"auto"}`))
|
||||
case "required", "any":
|
||||
out, _ = sjson.SetRawBytes(out, "tool_choice", []byte(`{"type":"any"}`))
|
||||
}
|
||||
case gjson.JSON:
|
||||
toolType := strings.ToLower(strings.TrimSpace(toolChoice.Get("type").String()))
|
||||
switch toolType {
|
||||
case "auto":
|
||||
out, _ = sjson.SetRawBytes(out, "tool_choice", []byte(`{"type":"auto"}`))
|
||||
case "required", "any":
|
||||
out, _ = sjson.SetRawBytes(out, "tool_choice", []byte(`{"type":"any"}`))
|
||||
case "function", "tool":
|
||||
name := toolChoice.Get("name").String()
|
||||
if name == "" {
|
||||
name = toolChoice.Get("function.name").String()
|
||||
}
|
||||
if name != "" {
|
||||
choice := []byte(`{"type":"tool","name":""}`)
|
||||
choice, _ = sjson.SetBytes(choice, "name", name)
|
||||
out, _ = sjson.SetRawBytes(out, "tool_choice", choice)
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func interactionsClaudeToolID(step gjson.Result) string {
|
||||
for _, path := range []string{"call_id", "id", "tool_use_id"} {
|
||||
if value := step.Get(path).String(); value != "" {
|
||||
return util.SanitizeClaudeToolID(value)
|
||||
}
|
||||
}
|
||||
if name := step.Get("name").String(); name != "" {
|
||||
return util.SanitizeClaudeToolID("toolu_" + name)
|
||||
}
|
||||
return "toolu_interactions"
|
||||
}
|
||||
|
||||
func interactionsClaudeText(value gjson.Result) string {
|
||||
if !value.Exists() {
|
||||
return ""
|
||||
}
|
||||
if value.Type == gjson.String {
|
||||
return value.String()
|
||||
}
|
||||
if text := value.Get("text"); text.Exists() {
|
||||
return text.String()
|
||||
}
|
||||
if thinking := value.Get("thinking"); thinking.Exists() {
|
||||
return thinking.String()
|
||||
}
|
||||
if content := value.Get("content"); content.Exists() {
|
||||
return interactionsClaudeText(content)
|
||||
}
|
||||
if parts := value.Get("parts"); parts.Exists() && parts.IsArray() {
|
||||
var builder strings.Builder
|
||||
parts.ForEach(func(_, part gjson.Result) bool {
|
||||
text := interactionsClaudeText(part)
|
||||
if text == "" {
|
||||
return true
|
||||
}
|
||||
if builder.Len() > 0 {
|
||||
builder.WriteByte('\n')
|
||||
}
|
||||
builder.WriteString(text)
|
||||
return true
|
||||
})
|
||||
return builder.String()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func interactionsClaudeMediaPart(part gjson.Result, claudeType string) ([]byte, bool) {
|
||||
mimeType := firstClaudeInteractionsExisting(part, "mime_type", "mimeType", "media_type", "mediaType").String()
|
||||
data := firstClaudeInteractionsExisting(part, "data", "file_data", "fileData").String()
|
||||
if source := part.Get("source"); source.Exists() {
|
||||
if mimeType == "" {
|
||||
mimeType = source.Get("media_type").String()
|
||||
}
|
||||
if data == "" {
|
||||
data = source.Get("data").String()
|
||||
}
|
||||
}
|
||||
if mimeType == "" || data == "" {
|
||||
return nil, false
|
||||
}
|
||||
out := []byte(`{"type":"","source":{"type":"base64","media_type":"","data":""}}`)
|
||||
out, _ = sjson.SetBytes(out, "type", claudeType)
|
||||
out, _ = sjson.SetBytes(out, "source.media_type", mimeType)
|
||||
out, _ = sjson.SetBytes(out, "source.data", data)
|
||||
return out, true
|
||||
}
|
||||
|
||||
func firstClaudeInteractionsExisting(root gjson.Result, paths ...string) gjson.Result {
|
||||
for _, path := range paths {
|
||||
if value := root.Get(path); value.Exists() {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return gjson.Result{}
|
||||
}
|
||||
|
|
@ -0,0 +1,595 @@
|
|||
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 ""
|
||||
}
|
||||
|
|
@ -0,0 +1,238 @@
|
|||
package interactions
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
func TestConvertInteractionsRequestToClaudeWithToolMessagesDirect(t *testing.T) {
|
||||
out := ConvertInteractionsRequestToClaude("claude-test", []byte(`{"model":"claude-test","system_instruction":"be brief","input":[{"type":"user_input","content":[{"type":"text","text":"hi"}]},{"type":"function_call","name":"lookup","call_id":"toolu_1","arguments":{"q":"x"}},{"type":"function_result","name":"lookup","call_id":"toolu_1","result":{"ok":true}}]}`), false)
|
||||
if got := gjson.GetBytes(out, "system").String(); got != "be brief" {
|
||||
t.Fatalf("system = %q, want be brief. Output: %s", got, string(out))
|
||||
}
|
||||
if got := gjson.GetBytes(out, "messages.0.content.0.text").String(); got != "hi" {
|
||||
t.Fatalf("messages.0.content.0.text = %q, want hi. Output: %s", got, string(out))
|
||||
}
|
||||
if got := gjson.GetBytes(out, "messages.1.content.0.type").String(); got != "tool_use" {
|
||||
t.Fatalf("messages.1.content.0.type = %q, want tool_use. Output: %s", got, string(out))
|
||||
}
|
||||
if got := gjson.GetBytes(out, "messages.2.content.0.type").String(); got != "tool_result" {
|
||||
t.Fatalf("messages.2.content.0.type = %q, want tool_result. Output: %s", got, string(out))
|
||||
}
|
||||
if got := gjson.GetBytes(out, "messages.2.content.0.tool_use_id").String(); got != "toolu_1" {
|
||||
t.Fatalf("tool_use_id = %q, want toolu_1. Output: %s", got, string(out))
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertInteractionsRequestToClaudeGroupsConsecutiveRoleTurns(t *testing.T) {
|
||||
raw := []byte(`{
|
||||
"input":[
|
||||
{"type":"thought","content":[{"type":"thinking","thinking":"reason"}]},
|
||||
{"type":"model_output","content":[{"type":"text","text":"answer"}]},
|
||||
{"type":"function_call","name":"first","call_id":"call_1","arguments":{}},
|
||||
{"type":"function_call","name":"second","call_id":"call_2","arguments":{}},
|
||||
{"type":"function_result","call_id":"call_1","result":{"value":"one"}},
|
||||
{"type":"function_result","call_id":"call_2","result":{"value":"two"}}
|
||||
]
|
||||
}`)
|
||||
out := ConvertInteractionsRequestToClaude("claude-test", raw, false)
|
||||
messages := gjson.GetBytes(out, "messages").Array()
|
||||
if len(messages) != 2 {
|
||||
t.Fatalf("message count = %d, want 2. Output: %s", len(messages), string(out))
|
||||
}
|
||||
assistantContent := messages[0].Get("content").Array()
|
||||
wantAssistantTypes := []string{"thinking", "text", "tool_use", "tool_use"}
|
||||
if len(assistantContent) != len(wantAssistantTypes) {
|
||||
t.Fatalf("assistant content count = %d, want %d. Output: %s", len(assistantContent), len(wantAssistantTypes), string(out))
|
||||
}
|
||||
for i, wantType := range wantAssistantTypes {
|
||||
if got := assistantContent[i].Get("type").String(); got != wantType {
|
||||
t.Fatalf("assistant content[%d].type = %q, want %q", i, got, wantType)
|
||||
}
|
||||
}
|
||||
userContent := messages[1].Get("content").Array()
|
||||
if len(userContent) != 2 {
|
||||
t.Fatalf("user content count = %d, want 2. Output: %s", len(userContent), string(out))
|
||||
}
|
||||
for i, wantID := range []string{"call_1", "call_2"} {
|
||||
if got := userContent[i].Get("tool_use_id").String(); got != wantID {
|
||||
t.Fatalf("user content[%d].tool_use_id = %q, want %q", i, got, wantID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertInteractionsRequestToClaudeDoesNotMergeAcrossRoleChanges(t *testing.T) {
|
||||
raw := []byte(`{
|
||||
"input":[
|
||||
{"type":"model_output","content":"first assistant"},
|
||||
{"type":"user_input","content":"user reply"},
|
||||
{"type":"model_output","content":"second assistant"}
|
||||
]
|
||||
}`)
|
||||
out := ConvertInteractionsRequestToClaude("claude-test", raw, false)
|
||||
messages := gjson.GetBytes(out, "messages").Array()
|
||||
if len(messages) != 3 {
|
||||
t.Fatalf("message count = %d, want 3. Output: %s", len(messages), string(out))
|
||||
}
|
||||
for i, wantRole := range []string{"assistant", "user", "assistant"} {
|
||||
if got := messages[i].Get("role").String(); got != wantRole {
|
||||
t.Fatalf("messages[%d].role = %q, want %q", i, got, wantRole)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertInteractionsRequestToClaudeStringInputDirect(t *testing.T) {
|
||||
out := ConvertInteractionsRequestToClaude("claude-test", []byte(`{"model":"claude-test","input":"hello"}`), false)
|
||||
if got := gjson.GetBytes(out, "messages.0.role").String(); got != "user" {
|
||||
t.Fatalf("messages.0.role = %q, want user. Output: %s", got, string(out))
|
||||
}
|
||||
if got := gjson.GetBytes(out, "messages.0.content.0.text").String(); got != "hello" {
|
||||
t.Fatalf("messages.0.content.0.text = %q, want hello. Output: %s", got, string(out))
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertInteractionsRequestToClaudeMapsGenerationConfigToolsAndStreamDirect(t *testing.T) {
|
||||
out := ConvertInteractionsRequestToClaude("claude-test", []byte(`{"model":"claude-test","stream":true,"input":[{"type":"user_input","content":[{"type":"text","text":"hi"}]}],"tools":[{"type":"function","name":"lookup","description":"Lookup data","parameters":{"type":"object","properties":{"q":{"type":"string"}}}}],"generation_config":{"max_output_tokens":99,"top_p":0.7,"stop_sequences":["END"],"tool_choice":{"type":"function","name":"lookup"},"thinking_level":"high"}}`), false)
|
||||
if !gjson.GetBytes(out, "stream").Bool() {
|
||||
t.Fatalf("stream should be true when request body asks for stream. Output: %s", string(out))
|
||||
}
|
||||
if got := gjson.GetBytes(out, "max_tokens").Int(); got != 99 {
|
||||
t.Fatalf("max_tokens = %d, want 99. Output: %s", got, string(out))
|
||||
}
|
||||
if got := gjson.GetBytes(out, "tools.0.input_schema.properties.q.type").String(); got != "string" {
|
||||
t.Fatalf("tool schema type = %q, want string. Output: %s", got, string(out))
|
||||
}
|
||||
if got := gjson.GetBytes(out, "tool_choice.name").String(); got != "lookup" {
|
||||
t.Fatalf("tool_choice.name = %q, want lookup. Output: %s", got, string(out))
|
||||
}
|
||||
if got := gjson.GetBytes(out, "thinking.type").String(); got == "" {
|
||||
t.Fatalf("thinking config was not mapped. Output: %s", string(out))
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertInteractionsRequestToClaudeAcceptsImageContent(t *testing.T) {
|
||||
out := ConvertInteractionsRequestToClaude("claude-test", []byte(`{"model":"claude-test","input":[{"type":"user_input","content":[{"type":"image","mime_type":"image/png","data":"aGVsbG8="}]}]}`), false)
|
||||
if got := gjson.GetBytes(out, "messages.0.content.0.type").String(); got != "image" {
|
||||
t.Fatalf("content type = %q, want image. Output: %s", got, string(out))
|
||||
}
|
||||
if got := gjson.GetBytes(out, "messages.0.content.0.source.media_type").String(); got != "image/png" {
|
||||
t.Fatalf("media_type = %q, want image/png. Output: %s", got, string(out))
|
||||
}
|
||||
if got := gjson.GetBytes(out, "messages.0.content.0.source.data").String(); got != "aGVsbG8=" {
|
||||
t.Fatalf("data = %q, want aGVsbG8=. Output: %s", got, string(out))
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertInteractionsRequestToClaudePreservesNonImageMediaContent(t *testing.T) {
|
||||
out := ConvertInteractionsRequestToClaude("claude-test", []byte(`{"model":"claude-test","input":[{"type":"thought","content":[{"type":"audio","mime_type":"audio/wav","data":"UklGRg=="},{"type":"video","mime_type":"video/mp4","data":"AAAAIGZ0eXA="},{"type":"document","mime_type":"application/pdf","data":"JVBERi0="}]}]}`), false)
|
||||
|
||||
if got := gjson.GetBytes(out, "messages.0.role").String(); got != "assistant" {
|
||||
t.Fatalf("messages.0.role = %q, want assistant. Output: %s", got, string(out))
|
||||
}
|
||||
if got := gjson.GetBytes(out, "messages.0.content.0.type").String(); got != "text" {
|
||||
t.Fatalf("audio fallback type = %q, want text. Output: %s", got, string(out))
|
||||
}
|
||||
if got := gjson.GetBytes(out, "messages.0.content.1.type").String(); got != "text" {
|
||||
t.Fatalf("video fallback type = %q, want text. Output: %s", got, string(out))
|
||||
}
|
||||
if got := gjson.GetBytes(out, "messages.0.content.2.type").String(); got != "document" {
|
||||
t.Fatalf("document content type = %q, want document. Output: %s", got, string(out))
|
||||
}
|
||||
if gjson.GetBytes(out, "messages.0.content.#(type==\"image\")").Exists() {
|
||||
t.Fatalf("non-image media must not be converted to image. Output: %s", string(out))
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertClaudeResponseToInteractionsNonStream(t *testing.T) {
|
||||
raw := []byte(`{"id":"msg_1","model":"claude-test","content":[{"type":"thinking","thinking":"reasoning"},{"type":"text","text":"ok"},{"type":"tool_use","id":"toolu_1","name":"lookup","input":{"q":"x"}}],"usage":{"input_tokens":3,"output_tokens":2,"cache_read_input_tokens":1,"cache_creation_input_tokens":4,"thinking_tokens":5}}`)
|
||||
out := ConvertClaudeResponseToInteractionsNonStream(context.Background(), "claude-test", nil, nil, raw, nil)
|
||||
if got := gjson.GetBytes(out, "steps.0.type").String(); got != "thought" {
|
||||
t.Fatalf("steps.0.type = %q, want thought. Output: %s", got, string(out))
|
||||
}
|
||||
if got := gjson.GetBytes(out, "steps.1.content.0.text").String(); got != "ok" {
|
||||
t.Fatalf("steps.1.content.0.text = %q, want ok. Output: %s", got, string(out))
|
||||
}
|
||||
if got := gjson.GetBytes(out, "steps.2.call_id").String(); got != "toolu_1" {
|
||||
t.Fatalf("steps.2.call_id = %q, want toolu_1. Output: %s", got, string(out))
|
||||
}
|
||||
if got := gjson.GetBytes(out, "usage.total_tokens").Int(); got != 5 {
|
||||
t.Fatalf("usage.total_tokens = %d, want 5. Output: %s", got, string(out))
|
||||
}
|
||||
if got := gjson.GetBytes(out, "usage.total_cached_tokens").Int(); got != 5 {
|
||||
t.Fatalf("usage.total_cached_tokens = %d, want 5. Output: %s", got, string(out))
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertClaudeSSEToInteractionsNonStream(t *testing.T) {
|
||||
raw := []byte(`data: {"type":"message_start","message":{"id":"msg_1","model":"claude-test","usage":{"input_tokens":3,"output_tokens":0}}}
|
||||
data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}
|
||||
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"ok"}}
|
||||
data: {"type":"content_block_stop","index":0}
|
||||
data: {"type":"message_delta","usage":{"output_tokens":2}}`)
|
||||
out := ConvertClaudeResponseToInteractionsNonStream(context.Background(), "claude-test", nil, nil, raw, nil)
|
||||
if got := gjson.GetBytes(out, "steps.0.content.0.text").String(); got != "ok" {
|
||||
t.Fatalf("steps.0.content.0.text = %q, want ok. Output: %s", got, string(out))
|
||||
}
|
||||
if got := gjson.GetBytes(out, "usage.total_tokens").Int(); got != 5 {
|
||||
t.Fatalf("usage.total_tokens = %d, want 5. Output: %s", got, string(out))
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertClaudeResponseToInteractionsStreamMergesUsageAndStatus(t *testing.T) {
|
||||
var param any
|
||||
var events [][]byte
|
||||
for _, raw := range [][]byte{
|
||||
[]byte(`data: {"type":"message_start","message":{"id":"msg_1","model":"claude-test","usage":{"input_tokens":3,"output_tokens":0}}}`),
|
||||
[]byte(`data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}`),
|
||||
[]byte(`data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"ok"}}`),
|
||||
[]byte(`data: {"type":"content_block_stop","index":0}`),
|
||||
[]byte(`data: {"type":"message_delta","usage":{"output_tokens":2}}`),
|
||||
} {
|
||||
events = append(events, ConvertClaudeResponseToInteractions(context.Background(), "claude-test", nil, nil, raw, ¶m)...)
|
||||
}
|
||||
if payload := findClaudeInteractionsEventPayload(events, "interaction.status_update"); len(payload) == 0 {
|
||||
t.Fatalf("interaction.status_update event not found: %q", events)
|
||||
}
|
||||
payload := findClaudeInteractionsEventPayload(events, "interaction.completed")
|
||||
if got := gjson.GetBytes(payload, "interaction.usage.total_input_tokens").Int(); got != 3 {
|
||||
t.Fatalf("total_input_tokens = %d, want 3. Payload: %s", got, string(payload))
|
||||
}
|
||||
if got := gjson.GetBytes(payload, "interaction.usage.total_output_tokens").Int(); got != 2 {
|
||||
t.Fatalf("total_output_tokens = %d, want 2. Payload: %s", got, string(payload))
|
||||
}
|
||||
if got := gjson.GetBytes(payload, "interaction.usage.total_tokens").Int(); got != 5 {
|
||||
t.Fatalf("total_tokens = %d, want 5. Payload: %s", got, string(payload))
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertClaudeResponseToInteractionsStream(t *testing.T) {
|
||||
var param any
|
||||
events := ConvertClaudeResponseToInteractions(context.Background(), "claude-test", nil, nil, []byte(`data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"ok"}}`), ¶m)
|
||||
payload := findClaudeInteractionsEventPayload(events, "step.delta")
|
||||
if len(payload) == 0 {
|
||||
t.Fatalf("step.delta event not found: %q", events)
|
||||
}
|
||||
if got := gjson.GetBytes(payload, "delta.text").String(); got != "ok" {
|
||||
t.Fatalf("delta.text = %q, want ok. Payload: %s", got, string(payload))
|
||||
}
|
||||
}
|
||||
|
||||
func findClaudeInteractionsEventPayload(events [][]byte, eventType string) []byte {
|
||||
prefix := []byte("data:")
|
||||
for _, event := range events {
|
||||
for _, line := range bytes.Split(event, []byte("\n")) {
|
||||
line = bytes.TrimSpace(line)
|
||||
if !bytes.HasPrefix(line, prefix) {
|
||||
continue
|
||||
}
|
||||
payload := bytes.TrimSpace(line[len(prefix):])
|
||||
if gjson.GetBytes(payload, "event_type").String() == eventType || gjson.GetBytes(payload, "type").String() == eventType {
|
||||
return payload
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Loading…
Reference in a new issue