Files
2026-07-13 12:35:43 +08:00

364 lines
13 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"
)
type interactionsToGeminiStreamState struct {
ID string
Model string
ServiceTier string
StepNames map[int]string
StepIDs map[int]string
StepSignatures map[int]string
}
func ConvertGeminiResponseToInteractions(ctx context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) [][]byte {
return ConvertGeminiResponseToInteractionsStream(ctx, modelName, originalRequestRawJSON, requestRawJSON, rawJSON, param)
}
func ConvertGeminiResponseToInteractionsNonStream(_ context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) []byte {
return convertGeminiResponseToInteractionsNonStreamDirect(modelName, originalRequestRawJSON, requestRawJSON, rawJSON)
}
func ConvertInteractionsResponseToGemini(_ context.Context, modelName string, _, _, rawJSON []byte, param *any) [][]byte {
if param == nil {
var local any
param = &local
}
if *param == nil {
*param = &interactionsToGeminiStreamState{Model: modelName}
}
st := (*param).(*interactionsToGeminiStreamState)
st.ensureMaps()
return convertInteractionsEventToGemini(modelName, rawJSON, st)
}
func ConvertInteractionsResponseToGeminiNonStream(_ context.Context, modelName string, _, _, rawJSON []byte, _ *any) []byte {
root := gjson.ParseBytes(rawJSON)
interaction := root
if nested := root.Get("interaction"); nested.Exists() {
interaction = nested
}
st := &interactionsToGeminiStreamState{
ID: firstNonEmptyInteractionString(interaction.Get("id").String(), root.Get("id").String(), fmt.Sprintf("response_%d", time.Now().UnixNano())),
Model: firstNonEmptyInteractionString(interaction.Get("model").String(), root.Get("model").String(), modelName),
ServiceTier: firstNonEmptyInteractionString(interaction.Get("service_tier").String(), root.Get("service_tier").String()),
}
var parts [][]byte
steps := interaction.Get("steps")
if !steps.Exists() {
steps = root.Get("steps")
}
steps.ForEach(func(_, step gjson.Result) bool {
parts = append(parts, interactionsStepToGeminiParts(step)...)
return true
})
out := buildInteractionsGeminiChunk(st, modelName, parts, "STOP", translatorcommon.InteractionsUsage(root), true)
return out
}
func ConvertInteractionsRequestToInteractions(modelName string, inputRawJSON []byte, stream bool) []byte {
_ = modelName
_ = stream
return inputRawJSON
}
func ConvertInteractionsResponsePassthrough(_ context.Context, _ string, _, _, rawJSON []byte, _ *any) [][]byte {
if len(rawJSON) == 0 {
return nil
}
return [][]byte{rawJSON}
}
func ConvertInteractionsResponsePassthroughNonStream(_ context.Context, _ string, _, _, rawJSON []byte, _ *any) []byte {
return rawJSON
}
func convertInteractionsEventToGemini(modelName string, rawJSON []byte, st *interactionsToGeminiStreamState) [][]byte {
payload := interactionsGeminiSSEPayload(rawJSON)
if len(payload) == 0 {
return nil
}
root := gjson.ParseBytes(payload)
if !root.Exists() {
return nil
}
switch root.Get("event_type").String() {
case "interaction.created":
interaction := root.Get("interaction")
st.ID = firstNonEmptyInteractionString(st.ID, interaction.Get("id").String())
st.Model = firstNonEmptyInteractionString(st.Model, interaction.Get("model").String(), modelName)
case "step.start":
rememberInteractionsGeminiStep(root, st)
case "step.delta":
if chunk := interactionsStepDeltaToGeminiChunk(modelName, root, st); len(chunk) > 0 {
return [][]byte{chunk}
}
case "interaction.completed", "finish":
interaction := root.Get("interaction")
st.ID = firstNonEmptyInteractionString(st.ID, interaction.Get("id").String())
st.Model = firstNonEmptyInteractionString(st.Model, interaction.Get("model").String(), modelName)
st.ServiceTier = firstNonEmptyInteractionString(st.ServiceTier, interaction.Get("service_tier").String())
chunk := buildInteractionsGeminiChunk(st, modelName, nil, "STOP", translatorcommon.InteractionsUsage(root), true)
return [][]byte{chunk}
}
return nil
}
func rememberInteractionsGeminiStep(root gjson.Result, st *interactionsToGeminiStreamState) {
index := int(root.Get("index").Int())
step := root.Get("step")
st.StepNames[index] = step.Get("name").String()
st.StepIDs[index] = firstNonEmptyInteractionString(step.Get("call_id").String(), step.Get("id").String())
st.StepSignatures[index] = firstNonEmptyInteractionString(step.Get("signature").String(), step.Get("thoughtSignature").String(), step.Get("thought_signature").String())
}
func interactionsStepDeltaToGeminiChunk(modelName string, root gjson.Result, st *interactionsToGeminiStreamState) []byte {
index := int(root.Get("index").Int())
delta := root.Get("delta")
switch delta.Get("type").String() {
case "arguments_delta":
part := []byte(`{"functionCall":{"name":"","args":{}}}`)
part, _ = sjson.SetBytes(part, "functionCall.name", firstNonEmptyInteractionString(st.StepNames[index], root.Get("step.name").String()))
if id := st.StepIDs[index]; id != "" {
part, _ = sjson.SetBytes(part, "functionCall.id", id)
}
if signature := st.StepSignatures[index]; signature != "" {
part, _ = sjson.SetBytes(part, "thoughtSignature", signature)
}
arguments := strings.TrimSpace(delta.Get("arguments").String())
if arguments != "" && gjson.Valid(arguments) {
part, _ = sjson.SetRawBytes(part, "functionCall.args", []byte(arguments))
}
return buildInteractionsGeminiChunk(st, modelName, [][]byte{part}, "", gjson.Result{}, false)
case "text":
text := firstNonEmptyInteractionString(delta.Get("text").String(), delta.Get("content.text").String())
if text == "" {
return nil
}
return buildInteractionsGeminiChunk(st, modelName, [][]byte{geminiTextPartJSON(text, false)}, "", gjson.Result{}, false)
case "thought_summary":
text := firstNonEmptyInteractionString(delta.Get("content.text").String(), delta.Get("text").String())
if text == "" {
return nil
}
return buildInteractionsGeminiChunk(st, modelName, [][]byte{geminiTextPartJSON(text, true)}, "", gjson.Result{}, false)
case "thought_signature":
signature := firstNonEmptyInteractionString(delta.Get("signature").String(), delta.Get("thought_signature").String(), delta.Get("thoughtSignature").String())
if signature == "" {
return nil
}
st.StepSignatures[index] = signature
part := geminiTextPartJSON("", true)
part, _ = sjson.SetBytes(part, "thoughtSignature", signature)
return buildInteractionsGeminiChunk(st, modelName, [][]byte{part}, "", gjson.Result{}, false)
}
return nil
}
func interactionsStepToGeminiParts(step gjson.Result) [][]byte {
switch step.Get("type").String() {
case "function_call":
return [][]byte{interactionsFunctionCallStepToGeminiPart(step)}
case "function_result":
return [][]byte{interactionsFunctionResponseStepToGeminiPart(step)}
case "thought":
return interactionsContentToGeminiParts(step.Get("content"), true)
default:
return interactionsContentToGeminiParts(step.Get("content"), false)
}
}
func interactionsContentToGeminiParts(content gjson.Result, thought bool) [][]byte {
var parts [][]byte
if !content.Exists() {
return parts
}
if content.Type == gjson.String {
return [][]byte{geminiTextPartJSON(content.String(), thought)}
}
if content.IsObject() {
if part := interactionsContentPartToGeminiPart(content, thought); len(part) > 0 {
parts = append(parts, part)
}
return parts
}
if content.IsArray() {
content.ForEach(func(_, item gjson.Result) bool {
if part := interactionsContentPartToGeminiPart(item, thought); len(part) > 0 {
parts = append(parts, part)
}
return true
})
}
return parts
}
func interactionsFunctionCallStepToGeminiPart(step gjson.Result) []byte {
part := []byte(`{"functionCall":{"name":"","args":{}}}`)
part, _ = sjson.SetBytes(part, "functionCall.name", step.Get("name").String())
if id := firstNonEmptyInteractionString(step.Get("call_id").String(), step.Get("id").String()); id != "" {
part, _ = sjson.SetBytes(part, "functionCall.id", id)
}
if signature := firstNonEmptyInteractionString(step.Get("signature").String(), step.Get("thoughtSignature").String(), step.Get("thought_signature").String()); signature != "" {
part, _ = sjson.SetBytes(part, "thoughtSignature", signature)
}
part = setInteractionsGeminiRawObject(part, "functionCall.args", firstExistingInteractionResult(step, "arguments", "args"))
return part
}
func interactionsFunctionResponseStepToGeminiPart(step gjson.Result) []byte {
part := []byte(`{"functionResponse":{"name":"","response":{}}}`)
part, _ = sjson.SetBytes(part, "functionResponse.name", step.Get("name").String())
if id := firstNonEmptyInteractionString(step.Get("call_id").String(), step.Get("id").String()); id != "" {
part, _ = sjson.SetBytes(part, "functionResponse.id", id)
}
part = setInteractionsGeminiRawObject(part, "functionResponse.response", firstExistingInteractionResult(step, "result", "response"))
return part
}
func buildInteractionsGeminiChunk(st *interactionsToGeminiStreamState, modelName string, parts [][]byte, finishReason string, usage gjson.Result, includeEmptyPart bool) []byte {
out := []byte(`{"candidates":[{"content":{"parts":[],"role":"model"},"index":0}]}`)
if len(parts) == 0 && includeEmptyPart {
parts = append(parts, geminiTextPartJSON("", false))
}
for _, part := range parts {
if len(part) > 0 {
out, _ = sjson.SetRawBytes(out, "candidates.0.content.parts.-1", part)
}
}
if finishReason != "" {
out, _ = sjson.SetBytes(out, "candidates.0.finishReason", finishReason)
}
if model := firstNonEmptyInteractionString(st.Model, modelName); model != "" {
out, _ = sjson.SetBytes(out, "modelVersion", model)
}
if id := st.ID; id != "" {
out, _ = sjson.SetBytes(out, "responseId", id)
}
if st.ServiceTier != "" {
out, _ = sjson.SetBytes(out, "usageMetadata.serviceTier", st.ServiceTier)
}
return setGeminiUsageMetadataFromInteractionsUsage(out, usage)
}
func setGeminiUsageMetadataFromInteractionsUsage(out []byte, usage gjson.Result) []byte {
if !usage.Exists() {
return out
}
inputTokens, hasInputTokens := interactionsUsageInt(usage, "input_tokens", "total_input_tokens")
outputTokens, hasOutputTokens := interactionsUsageInt(usage, "output_tokens", "total_output_tokens")
totalTokens, hasTotalTokens := interactionsUsageInt(usage, "total_tokens")
if hasInputTokens {
out, _ = sjson.SetBytes(out, "usageMetadata.promptTokenCount", inputTokens)
out, _ = sjson.SetRawBytes(out, "usageMetadata.promptTokensDetails", []byte(fmt.Sprintf(`[{"modality":"TEXT","tokenCount":%d}]`, inputTokens)))
}
if hasOutputTokens {
out, _ = sjson.SetBytes(out, "usageMetadata.candidatesTokenCount", outputTokens)
}
if hasTotalTokens {
out, _ = sjson.SetBytes(out, "usageMetadata.totalTokenCount", totalTokens)
} else if hasInputTokens || hasOutputTokens {
out, _ = sjson.SetBytes(out, "usageMetadata.totalTokenCount", inputTokens+outputTokens)
}
if thoughtTokens, ok := interactionsUsageInt(usage, "reasoning_tokens", "total_thought_tokens"); ok {
out, _ = sjson.SetBytes(out, "usageMetadata.thoughtsTokenCount", thoughtTokens)
}
if cachedTokens, ok := interactionsUsageInt(usage, "cached_tokens", "total_cached_tokens"); ok {
out, _ = sjson.SetBytes(out, "usageMetadata.cachedContentTokenCount", cachedTokens)
}
return out
}
func interactionsGeminiSSEPayload(rawJSON []byte) []byte {
trimmed := bytes.TrimSpace(rawJSON)
if len(trimmed) == 0 || bytes.Equal(trimmed, []byte("[DONE]")) {
return nil
}
if bytes.HasPrefix(trimmed, []byte("{")) {
return trimmed
}
var payload []byte
for _, line := range bytes.Split(trimmed, []byte{'\n'}) {
line = bytes.TrimSpace(bytes.TrimRight(line, "\r"))
if !bytes.HasPrefix(line, []byte("data:")) {
continue
}
data := bytes.TrimSpace(line[len("data:"):])
if len(data) == 0 || bytes.Equal(data, []byte("[DONE]")) {
continue
}
if len(payload) > 0 {
payload = append(payload, '\n')
}
payload = append(payload, data...)
}
return payload
}
func interactionsUsageInt(usage gjson.Result, paths ...string) (int64, bool) {
for _, path := range paths {
if value := usage.Get(path); value.Exists() {
return value.Int(), true
}
}
return 0, false
}
func firstExistingInteractionResult(root gjson.Result, paths ...string) gjson.Result {
for _, path := range paths {
if value := root.Get(path); value.Exists() {
return value
}
}
return gjson.Result{}
}
func setInteractionsGeminiRawObject(out []byte, path string, value gjson.Result) []byte {
if !value.Exists() {
out, _ = sjson.SetRawBytes(out, path, []byte(`{}`))
return out
}
if value.Type == gjson.String {
raw := strings.TrimSpace(value.String())
if raw != "" && gjson.Valid(raw) {
out, _ = sjson.SetRawBytes(out, path, []byte(raw))
return out
}
}
if value.Raw != "" {
out, _ = sjson.SetRawBytes(out, path, []byte(value.Raw))
}
return out
}
func firstNonEmptyInteractionString(values ...string) string {
for _, value := range values {
if strings.TrimSpace(value) != "" {
return value
}
}
return ""
}
func (st *interactionsToGeminiStreamState) ensureMaps() {
if st.StepNames == nil {
st.StepNames = make(map[int]string)
}
if st.StepIDs == nil {
st.StepIDs = make(map[int]string)
}
if st.StepSignatures == nil {
st.StepSignatures = make(map[int]string)
}
}