Add projects
This commit is contained in:
parent
2d3a9ad623
commit
8b607dd700
1802 changed files with 503346 additions and 2 deletions
19
backend/internal/translator/interactions/claude/init.go
Normal file
19
backend/internal/translator/interactions/claude/init.go
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
package claude
|
||||
|
||||
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(
|
||||
Claude,
|
||||
Interactions,
|
||||
ConvertClaudeRequestToInteractions,
|
||||
interfaces.TranslateResponse{
|
||||
Stream: ConvertInteractionsResponseToClaude,
|
||||
NonStream: ConvertInteractionsResponseToClaudeNonStream,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package claude
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
func TestConvertClaudeRequestToInteractionsWithCompatPreservesEmptyThinking(t *testing.T) {
|
||||
payload := []byte(`{"messages":[{"role":"assistant","content":[{"type":"thinking","thinking":"","signature":""}]}]}`)
|
||||
|
||||
withoutCompat := ConvertClaudeRequestToInteractions("deepseek-v4", payload, false)
|
||||
if gjson.GetBytes(withoutCompat, "input.#").Int() != 0 {
|
||||
t.Fatalf("default translation preserved empty thinking: %s", withoutCompat)
|
||||
}
|
||||
|
||||
withCompat := ConvertClaudeRequestToInteractionsWithCompat("deepseek-v4", payload, false)
|
||||
if gjson.GetBytes(withCompat, "input.0.type").String() != "thought" {
|
||||
t.Fatalf("compat translation missing thought step: %s", withCompat)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,310 @@
|
|||
package claude
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
translatorcommon "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/common"
|
||||
"github.com/tidwall/gjson"
|
||||
"github.com/tidwall/sjson"
|
||||
)
|
||||
|
||||
func ConvertClaudeRequestToInteractions(modelName string, inputRawJSON []byte, stream bool) []byte {
|
||||
return convertClaudeRequestToInteractions(modelName, inputRawJSON, stream, false)
|
||||
}
|
||||
|
||||
// ConvertClaudeRequestToInteractionsWithCompat preserves empty assistant
|
||||
// thinking blocks for configured compatibility endpoints.
|
||||
func ConvertClaudeRequestToInteractionsWithCompat(modelName string, inputRawJSON []byte, stream bool) []byte {
|
||||
return convertClaudeRequestToInteractions(modelName, inputRawJSON, stream, true)
|
||||
}
|
||||
|
||||
func convertClaudeRequestToInteractions(modelName string, inputRawJSON []byte, stream, preserveEmptyThinkingBlocks bool) []byte {
|
||||
root := gjson.ParseBytes(inputRawJSON)
|
||||
out := []byte(`{"model":"","input":[]}`)
|
||||
out, _ = sjson.SetBytes(out, "model", firstNonEmpty(modelName, root.Get("model").String()))
|
||||
if streamValue, ok := claudeRequestStreamValue(root, stream); ok {
|
||||
out, _ = sjson.SetBytes(out, "stream", streamValue)
|
||||
}
|
||||
out = copyClaudeSystemToInteractions(out, root)
|
||||
out = copyClaudeGenerationConfigToInteractions(out, root)
|
||||
out = appendClaudeMessagesToInteractions(out, root.Get("messages"), preserveEmptyThinkingBlocks)
|
||||
out = copyClaudeToolsToInteractions(out, root)
|
||||
return out
|
||||
}
|
||||
|
||||
func claudeRequestStreamValue(root gjson.Result, stream bool) (bool, bool) {
|
||||
if value := root.Get("stream"); value.Exists() {
|
||||
return value.Bool(), true
|
||||
}
|
||||
if stream {
|
||||
return true, true
|
||||
}
|
||||
return false, false
|
||||
}
|
||||
|
||||
func copyClaudeSystemToInteractions(out []byte, root gjson.Result) []byte {
|
||||
text := claudeText(root.Get("system"))
|
||||
if text == "" {
|
||||
return out
|
||||
}
|
||||
out, _ = sjson.SetBytes(out, "system_instruction", text)
|
||||
return out
|
||||
}
|
||||
|
||||
func copyClaudeGenerationConfigToInteractions(out []byte, root gjson.Result) []byte {
|
||||
out = copyClaudeJSONField(out, root, "max_tokens", "generation_config.max_output_tokens")
|
||||
out = copyClaudeJSONField(out, root, "temperature", "generation_config.temperature")
|
||||
out = copyClaudeJSONField(out, root, "top_p", "generation_config.top_p")
|
||||
out = copyClaudeJSONField(out, root, "stop_sequences", "generation_config.stop_sequences")
|
||||
out = copyClaudeThinkingToInteractions(out, root)
|
||||
return copyClaudeToolChoiceToInteractions(out, root.Get("tool_choice"))
|
||||
}
|
||||
|
||||
func copyClaudeJSONField(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 copyClaudeThinkingToInteractions(out []byte, root gjson.Result) []byte {
|
||||
thinking := root.Get("thinking")
|
||||
if thinking.Exists() {
|
||||
switch strings.ToLower(strings.TrimSpace(thinking.Get("type").String())) {
|
||||
case "disabled":
|
||||
out, _ = sjson.SetBytes(out, "generation_config.thinking_level", "none")
|
||||
case "enabled":
|
||||
if budget := thinking.Get("budget_tokens"); budget.Exists() {
|
||||
out, _ = sjson.SetRawBytes(out, "generation_config.thinking_config.thinking_budget", []byte(budget.Raw))
|
||||
} else {
|
||||
out, _ = sjson.SetBytes(out, "generation_config.thinking_level", "high")
|
||||
}
|
||||
case "adaptive":
|
||||
out, _ = sjson.SetBytes(out, "generation_config.thinking_level", "auto")
|
||||
}
|
||||
}
|
||||
if effort := root.Get("output_config.effort"); effort.Exists() && effort.Type == gjson.String {
|
||||
out, _ = sjson.SetBytes(out, "generation_config.thinking_level", strings.ToLower(strings.TrimSpace(effort.String())))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func copyClaudeToolChoiceToInteractions(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.SetBytes(out, "generation_config.tool_choice", "auto")
|
||||
case "any", "required":
|
||||
out, _ = sjson.SetBytes(out, "generation_config.tool_choice", "required")
|
||||
}
|
||||
case gjson.JSON:
|
||||
toolType := strings.ToLower(strings.TrimSpace(toolChoice.Get("type").String()))
|
||||
switch toolType {
|
||||
case "auto":
|
||||
out, _ = sjson.SetBytes(out, "generation_config.tool_choice", "auto")
|
||||
case "any", "required":
|
||||
out, _ = sjson.SetBytes(out, "generation_config.tool_choice", "required")
|
||||
case "tool":
|
||||
name := strings.TrimSpace(toolChoice.Get("name").String())
|
||||
if name != "" {
|
||||
choice := []byte(`{"type":"function","name":""}`)
|
||||
choice, _ = sjson.SetBytes(choice, "name", name)
|
||||
out, _ = sjson.SetRawBytes(out, "generation_config.tool_choice", choice)
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func appendClaudeMessagesToInteractions(out []byte, messages gjson.Result, preserveEmptyThinkingBlocks bool) []byte {
|
||||
if !messages.Exists() || !messages.IsArray() {
|
||||
return out
|
||||
}
|
||||
inputItems := translatorcommon.NewRawArrayItems(messages.Get("#").Int())
|
||||
messages.ForEach(func(_, message gjson.Result) bool {
|
||||
appendClaudeMessageToInteractions(&inputItems, message, preserveEmptyThinkingBlocks)
|
||||
return true
|
||||
})
|
||||
out = translatorcommon.SetRawArrayItems(out, "input", inputItems)
|
||||
return out
|
||||
}
|
||||
|
||||
func appendClaudeMessageToInteractions(items *[][]byte, message gjson.Result, preserveEmptyThinkingBlocks bool) {
|
||||
role := strings.ToLower(strings.TrimSpace(message.Get("role").String()))
|
||||
defaultStepType := "user_input"
|
||||
if role == "assistant" {
|
||||
defaultStepType = "model_output"
|
||||
}
|
||||
content := message.Get("content")
|
||||
if content.Type == gjson.String {
|
||||
step := []byte(`{"type":"","content":[{"type":"text","text":""}]}`)
|
||||
step, _ = sjson.SetBytes(step, "type", defaultStepType)
|
||||
step, _ = sjson.SetBytes(step, "content.0.text", content.String())
|
||||
*items = append(*items, step)
|
||||
return
|
||||
}
|
||||
if !content.IsArray() {
|
||||
return
|
||||
}
|
||||
stepContent := make([][]byte, 0, 4)
|
||||
flushContent := func() {
|
||||
if len(stepContent) == 0 {
|
||||
return
|
||||
}
|
||||
step := []byte(`{"type":"","content":[]}`)
|
||||
step, _ = sjson.SetBytes(step, "type", defaultStepType)
|
||||
step, _ = sjson.SetRawBytes(step, "content", translatorcommon.JoinRawArray(stepContent))
|
||||
*items = append(*items, step)
|
||||
stepContent = stepContent[:0]
|
||||
}
|
||||
content.ForEach(func(_, part gjson.Result) bool {
|
||||
partType := strings.ToLower(strings.TrimSpace(part.Get("type").String()))
|
||||
switch partType {
|
||||
case "text":
|
||||
if text := part.Get("text").String(); text != "" {
|
||||
contentPart := []byte(`{"type":"text","text":""}`)
|
||||
contentPart, _ = sjson.SetBytes(contentPart, "text", text)
|
||||
stepContent = append(stepContent, contentPart)
|
||||
}
|
||||
case "thinking":
|
||||
flushContent()
|
||||
text := part.Get("thinking").String()
|
||||
if text != "" || preserveEmptyThinkingBlocks {
|
||||
step := []byte(`{"type":"thought","content":[{"type":"text","text":""}]}`)
|
||||
step, _ = sjson.SetBytes(step, "content.0.text", text)
|
||||
*items = append(*items, step)
|
||||
}
|
||||
case "image", "document":
|
||||
if mediaPart, ok := claudeMediaPartToInteractions(part, partType); ok {
|
||||
stepContent = append(stepContent, mediaPart)
|
||||
}
|
||||
case "tool_use":
|
||||
flushContent()
|
||||
*items = append(*items, claudeToolUseToInteractions(part))
|
||||
case "tool_result":
|
||||
flushContent()
|
||||
*items = append(*items, claudeToolResultToInteractions(part))
|
||||
}
|
||||
return true
|
||||
})
|
||||
flushContent()
|
||||
}
|
||||
|
||||
func claudeMediaPartToInteractions(part gjson.Result, partType string) ([]byte, bool) {
|
||||
source := part.Get("source")
|
||||
mimeType := source.Get("media_type").String()
|
||||
data := source.Get("data").String()
|
||||
if mimeType == "" || data == "" {
|
||||
return nil, false
|
||||
}
|
||||
out := []byte(`{"type":"","mime_type":"","data":""}`)
|
||||
out, _ = sjson.SetBytes(out, "type", partType)
|
||||
out, _ = sjson.SetBytes(out, "mime_type", mimeType)
|
||||
out, _ = sjson.SetBytes(out, "data", data)
|
||||
return out, true
|
||||
}
|
||||
|
||||
func claudeToolUseToInteractions(part gjson.Result) []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)
|
||||
}
|
||||
input := part.Get("input")
|
||||
if input.Exists() && input.IsObject() {
|
||||
step, _ = sjson.SetRawBytes(step, "arguments", []byte(input.Raw))
|
||||
}
|
||||
return step
|
||||
}
|
||||
|
||||
func claudeToolResultToInteractions(part gjson.Result) []byte {
|
||||
step := []byte(`{"type":"function_result","call_id":"","result":""}`)
|
||||
if id := part.Get("tool_use_id").String(); id != "" {
|
||||
step, _ = sjson.SetBytes(step, "id", id)
|
||||
step, _ = sjson.SetBytes(step, "call_id", id)
|
||||
}
|
||||
result := part.Get("content")
|
||||
if result.Exists() {
|
||||
switch {
|
||||
case result.Type == gjson.String:
|
||||
step, _ = sjson.SetBytes(step, "result", result.String())
|
||||
case result.IsArray():
|
||||
contentItems := make([][]byte, 0, 4)
|
||||
result.ForEach(func(_, item gjson.Result) bool {
|
||||
if item.Get("type").String() == "text" {
|
||||
contentPart := []byte(`{"type":"text","text":""}`)
|
||||
contentPart, _ = sjson.SetBytes(contentPart, "text", item.Get("text").String())
|
||||
contentItems = append(contentItems, contentPart)
|
||||
}
|
||||
return true
|
||||
})
|
||||
step, _ = sjson.SetRawBytes(step, "result", translatorcommon.JoinRawArray(contentItems))
|
||||
default:
|
||||
step, _ = sjson.SetRawBytes(step, "result", []byte(result.Raw))
|
||||
}
|
||||
}
|
||||
return step
|
||||
}
|
||||
|
||||
func copyClaudeToolsToInteractions(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 {
|
||||
name := strings.TrimSpace(tool.Get("name").String())
|
||||
if name == "" {
|
||||
return true
|
||||
}
|
||||
item := []byte(`{"type":"function","name":"","parameters":{}}`)
|
||||
item, _ = sjson.SetBytes(item, "name", name)
|
||||
if desc := tool.Get("description"); desc.Exists() {
|
||||
item, _ = sjson.SetBytes(item, "description", desc.String())
|
||||
}
|
||||
if schema := tool.Get("input_schema"); schema.Exists() && schema.IsObject() {
|
||||
item, _ = sjson.SetRawBytes(item, "parameters", []byte(schema.Raw))
|
||||
}
|
||||
toolItems = append(toolItems, item)
|
||||
return true
|
||||
})
|
||||
if len(toolItems) > 0 {
|
||||
out, _ = sjson.SetRawBytes(out, "tools", translatorcommon.JoinRawArray(toolItems))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func claudeText(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 value.IsArray() {
|
||||
var builder strings.Builder
|
||||
value.ForEach(func(_, item gjson.Result) bool {
|
||||
text := claudeText(item)
|
||||
if text == "" {
|
||||
return true
|
||||
}
|
||||
if builder.Len() > 0 {
|
||||
builder.WriteByte('\n')
|
||||
}
|
||||
builder.WriteString(text)
|
||||
return true
|
||||
})
|
||||
return builder.String()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
|
@ -0,0 +1,403 @@
|
|||
package claude
|
||||
|
||||
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 interactionsToClaudeStreamState struct {
|
||||
ID string
|
||||
Model string
|
||||
Started bool
|
||||
ActiveBlock bool
|
||||
ActiveBlockType string
|
||||
BlockIndex int
|
||||
SawToolCall bool
|
||||
Completed bool
|
||||
Stopped bool
|
||||
Done bool
|
||||
StepTypes map[int]string
|
||||
ToolNames map[int]string
|
||||
ToolIDs map[int]string
|
||||
ToolSignatures map[int]string
|
||||
}
|
||||
|
||||
func ConvertInteractionsResponseToClaude(_ context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) [][]byte {
|
||||
_ = originalRequestRawJSON
|
||||
_ = requestRawJSON
|
||||
if param == nil {
|
||||
var local any
|
||||
param = &local
|
||||
}
|
||||
if *param == nil {
|
||||
*param = &interactionsToClaudeStreamState{Model: modelName}
|
||||
}
|
||||
st := (*param).(*interactionsToClaudeStreamState)
|
||||
st.Model = firstNonEmpty(st.Model, modelName)
|
||||
st.ensureMaps()
|
||||
return convertInteractionsEventToClaude(modelName, rawJSON, st)
|
||||
}
|
||||
|
||||
func ConvertInteractionsResponseToClaudeNonStream(_ context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) []byte {
|
||||
_ = originalRequestRawJSON
|
||||
_ = requestRawJSON
|
||||
root := gjson.ParseBytes(rawJSON)
|
||||
interaction := root
|
||||
if nested := root.Get("interaction"); nested.Exists() {
|
||||
interaction = nested
|
||||
}
|
||||
out := []byte(`{"id":"","type":"message","role":"assistant","model":"","content":[],"stop_reason":"end_turn","stop_sequence":null,"usage":{"input_tokens":0,"output_tokens":0}}`)
|
||||
out, _ = sjson.SetBytes(out, "id", firstNonEmpty(interaction.Get("id").String(), root.Get("id").String(), fmt.Sprintf("msg_%d", time.Now().UnixNano())))
|
||||
out, _ = sjson.SetBytes(out, "model", firstNonEmpty(interaction.Get("model").String(), modelName))
|
||||
steps := interaction.Get("steps")
|
||||
if !steps.Exists() {
|
||||
steps = root.Get("steps")
|
||||
}
|
||||
sawToolCall := false
|
||||
var contentBlocks [][]byte
|
||||
steps.ForEach(func(_, step gjson.Result) bool {
|
||||
switch step.Get("type").String() {
|
||||
case "thought":
|
||||
for _, text := range interactionsContentTexts(step.Get("content")) {
|
||||
block := []byte(`{"type":"thinking","thinking":""}`)
|
||||
block, _ = sjson.SetBytes(block, "thinking", text)
|
||||
contentBlocks = append(contentBlocks, block)
|
||||
}
|
||||
case "function_call":
|
||||
sawToolCall = true
|
||||
block := []byte(`{"type":"tool_use","id":"","name":"","input":{}}`)
|
||||
block, _ = sjson.SetBytes(block, "id", interactionsToolID(step))
|
||||
block, _ = sjson.SetBytes(block, "name", step.Get("name").String())
|
||||
if signature := interactionsSignature(step); signature != "" {
|
||||
block, _ = sjson.SetBytes(block, "signature", signature)
|
||||
}
|
||||
args := firstExisting(step, "arguments", "args")
|
||||
if args.Exists() && args.IsObject() {
|
||||
block, _ = sjson.SetRawBytes(block, "input", []byte(args.Raw))
|
||||
}
|
||||
contentBlocks = append(contentBlocks, block)
|
||||
default:
|
||||
for _, text := range interactionsContentTexts(step.Get("content")) {
|
||||
block := []byte(`{"type":"text","text":""}`)
|
||||
block, _ = sjson.SetBytes(block, "text", text)
|
||||
contentBlocks = append(contentBlocks, block)
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
if len(contentBlocks) > 0 {
|
||||
out = translatorcommon.SetRawArrayItems(out, "content", contentBlocks)
|
||||
}
|
||||
if sawToolCall {
|
||||
out, _ = sjson.SetBytes(out, "stop_reason", "tool_use")
|
||||
}
|
||||
out = setClaudeUsageFromInteractions(out, "usage", translatorcommon.InteractionsUsage(root))
|
||||
return out
|
||||
}
|
||||
|
||||
func convertInteractionsEventToClaude(modelName string, rawJSON []byte, st *interactionsToClaudeStreamState) [][]byte {
|
||||
payload := interactionsSSEPayload(rawJSON)
|
||||
if len(payload) == 0 {
|
||||
return nil
|
||||
}
|
||||
if bytes.Equal(bytes.TrimSpace(payload), []byte("[DONE]")) {
|
||||
return appendClaudeMessageStop(nil, st)
|
||||
}
|
||||
root := gjson.ParseBytes(payload)
|
||||
if !root.Exists() {
|
||||
return nil
|
||||
}
|
||||
switch root.Get("event_type").String() {
|
||||
case "interaction.created":
|
||||
interaction := root.Get("interaction")
|
||||
st.ID = firstNonEmpty(interaction.Get("id").String(), st.ID)
|
||||
st.Model = firstNonEmpty(interaction.Get("model").String(), st.Model, modelName)
|
||||
return appendClaudeMessageStart(nil, st)
|
||||
case "step.start":
|
||||
return interactionsStepStartToClaude(modelName, root, st)
|
||||
case "step.delta":
|
||||
return interactionsStepDeltaToClaude(modelName, root, st)
|
||||
case "step.stop":
|
||||
return appendClaudeContentBlockStop(nil, st)
|
||||
case "interaction.completed", "finish":
|
||||
return appendClaudeMessageDelta(nil, root, st)
|
||||
case "done":
|
||||
return appendClaudeMessageStop(nil, st)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func interactionsStepStartToClaude(modelName string, root gjson.Result, st *interactionsToClaudeStreamState) [][]byte {
|
||||
out := appendClaudeMessageStart(nil, st)
|
||||
out = appendClaudeContentBlockStop(out, st)
|
||||
index := int(root.Get("index").Int())
|
||||
step := root.Get("step")
|
||||
stepType := step.Get("type").String()
|
||||
st.StepTypes[index] = stepType
|
||||
switch stepType {
|
||||
case "function_call":
|
||||
st.SawToolCall = true
|
||||
st.ToolNames[index] = step.Get("name").String()
|
||||
st.ToolIDs[index] = interactionsToolID(step)
|
||||
st.ToolSignatures[index] = interactionsSignature(step)
|
||||
return appendClaudeToolBlockStart(out, index, st)
|
||||
case "thought":
|
||||
return appendClaudeContentBlockStart(out, "thinking", st)
|
||||
default:
|
||||
_ = modelName
|
||||
return appendClaudeContentBlockStart(out, "text", st)
|
||||
}
|
||||
}
|
||||
|
||||
func interactionsStepDeltaToClaude(modelName string, root gjson.Result, st *interactionsToClaudeStreamState) [][]byte {
|
||||
index := int(root.Get("index").Int())
|
||||
delta := root.Get("delta")
|
||||
switch delta.Get("type").String() {
|
||||
case "thought_summary":
|
||||
out := appendClaudeMessageStart(nil, st)
|
||||
out = ensureClaudeContentBlock(out, "thinking", st)
|
||||
text := firstNonEmpty(delta.Get("content.text").String(), delta.Get("text").String())
|
||||
return appendClaudeContentDelta(out, "thinking_delta", "thinking", text, st)
|
||||
case "thought_signature":
|
||||
if st.ActiveBlock && st.ActiveBlockType == "thinking" {
|
||||
return appendClaudeContentDelta(nil, "signature_delta", "signature", delta.Get("signature").String(), st)
|
||||
}
|
||||
case "arguments_delta":
|
||||
out := appendClaudeMessageStart(nil, st)
|
||||
if !st.ActiveBlock || st.ActiveBlockType != "tool_use" {
|
||||
out = appendClaudeContentBlockStop(out, st)
|
||||
if st.ToolNames[index] == "" {
|
||||
st.ToolNames[index] = root.Get("step.name").String()
|
||||
}
|
||||
if st.ToolIDs[index] == "" {
|
||||
st.ToolIDs[index] = fmt.Sprintf("toolu_%d", index)
|
||||
}
|
||||
out = appendClaudeToolBlockStart(out, index, st)
|
||||
}
|
||||
return appendClaudeContentDelta(out, "input_json_delta", "partial_json", delta.Get("arguments").String(), st)
|
||||
default:
|
||||
_ = modelName
|
||||
out := appendClaudeMessageStart(nil, st)
|
||||
out = ensureClaudeContentBlock(out, "text", st)
|
||||
return appendClaudeContentDelta(out, "text_delta", "text", delta.Get("text").String(), st)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func appendClaudeMessageStart(out [][]byte, st *interactionsToClaudeStreamState) [][]byte {
|
||||
if st.Started {
|
||||
return out
|
||||
}
|
||||
msg := []byte(`{"type":"message_start","message":{"id":"","type":"message","role":"assistant","content":[],"model":"","stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":0,"output_tokens":0}}}`)
|
||||
msg, _ = sjson.SetBytes(msg, "message.id", firstNonEmpty(st.ID, fmt.Sprintf("msg_%d", time.Now().UnixNano())))
|
||||
msg, _ = sjson.SetBytes(msg, "message.model", st.Model)
|
||||
st.Started = true
|
||||
return append(out, translatorcommon.AppendSSEEventBytes(nil, "message_start", msg, 3))
|
||||
}
|
||||
|
||||
func appendClaudeContentBlockStart(out [][]byte, blockType string, st *interactionsToClaudeStreamState) [][]byte {
|
||||
if st.ActiveBlock && st.ActiveBlockType == blockType {
|
||||
return out
|
||||
}
|
||||
out = appendClaudeContentBlockStop(out, st)
|
||||
var block []byte
|
||||
if blockType == "thinking" {
|
||||
block = []byte(`{"type":"content_block_start","index":0,"content_block":{"type":"thinking","thinking":""}}`)
|
||||
} else {
|
||||
block = []byte(`{"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}`)
|
||||
}
|
||||
block, _ = sjson.SetBytes(block, "index", st.BlockIndex)
|
||||
st.ActiveBlock = true
|
||||
st.ActiveBlockType = blockType
|
||||
return append(out, translatorcommon.AppendSSEEventBytes(nil, "content_block_start", block, 3))
|
||||
}
|
||||
|
||||
func appendClaudeToolBlockStart(out [][]byte, stepIndex int, st *interactionsToClaudeStreamState) [][]byte {
|
||||
out = appendClaudeContentBlockStop(out, st)
|
||||
block := []byte(`{"type":"content_block_start","index":0,"content_block":{"type":"tool_use","id":"","name":"","input":{}}}`)
|
||||
block, _ = sjson.SetBytes(block, "index", st.BlockIndex)
|
||||
block, _ = sjson.SetBytes(block, "content_block.id", firstNonEmpty(st.ToolIDs[stepIndex], fmt.Sprintf("toolu_%d", stepIndex)))
|
||||
block, _ = sjson.SetBytes(block, "content_block.name", st.ToolNames[stepIndex])
|
||||
if signature := st.ToolSignatures[stepIndex]; signature != "" {
|
||||
block, _ = sjson.SetBytes(block, "content_block.signature", signature)
|
||||
}
|
||||
st.ActiveBlock = true
|
||||
st.ActiveBlockType = "tool_use"
|
||||
return append(out, translatorcommon.AppendSSEEventBytes(nil, "content_block_start", block, 3))
|
||||
}
|
||||
|
||||
func ensureClaudeContentBlock(out [][]byte, blockType string, st *interactionsToClaudeStreamState) [][]byte {
|
||||
if st.ActiveBlock && st.ActiveBlockType == blockType {
|
||||
return out
|
||||
}
|
||||
return appendClaudeContentBlockStart(out, blockType, st)
|
||||
}
|
||||
|
||||
func appendClaudeContentDelta(out [][]byte, deltaType, field, value string, st *interactionsToClaudeStreamState) [][]byte {
|
||||
if value == "" && deltaType != "input_json_delta" {
|
||||
return out
|
||||
}
|
||||
delta := []byte(`{"type":"content_block_delta","index":0,"delta":{"type":""}}`)
|
||||
delta, _ = sjson.SetBytes(delta, "index", st.BlockIndex)
|
||||
delta, _ = sjson.SetBytes(delta, "delta.type", deltaType)
|
||||
delta, _ = sjson.SetBytes(delta, "delta."+field, value)
|
||||
return append(out, translatorcommon.AppendSSEEventBytes(nil, "content_block_delta", delta, 3))
|
||||
}
|
||||
|
||||
func appendClaudeContentBlockStop(out [][]byte, st *interactionsToClaudeStreamState) [][]byte {
|
||||
if !st.ActiveBlock {
|
||||
return out
|
||||
}
|
||||
stop := []byte(`{"type":"content_block_stop","index":0}`)
|
||||
stop, _ = sjson.SetBytes(stop, "index", st.BlockIndex)
|
||||
out = append(out, translatorcommon.AppendSSEEventBytes(nil, "content_block_stop", stop, 3))
|
||||
st.ActiveBlock = false
|
||||
st.ActiveBlockType = ""
|
||||
st.BlockIndex++
|
||||
return out
|
||||
}
|
||||
|
||||
func appendClaudeMessageDelta(out [][]byte, root gjson.Result, st *interactionsToClaudeStreamState) [][]byte {
|
||||
if st.Completed {
|
||||
return out
|
||||
}
|
||||
out = appendClaudeMessageStart(out, st)
|
||||
out = appendClaudeContentBlockStop(out, st)
|
||||
payload := []byte(`{"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"input_tokens":0,"output_tokens":0}}`)
|
||||
if st.SawToolCall {
|
||||
payload, _ = sjson.SetBytes(payload, "delta.stop_reason", "tool_use")
|
||||
}
|
||||
payload = setClaudeUsageFromInteractions(payload, "usage", translatorcommon.InteractionsUsage(root))
|
||||
out = append(out, translatorcommon.AppendSSEEventBytes(nil, "message_delta", payload, 3))
|
||||
st.Completed = true
|
||||
return out
|
||||
}
|
||||
|
||||
func appendClaudeMessageStop(out [][]byte, st *interactionsToClaudeStreamState) [][]byte {
|
||||
if st.Done {
|
||||
return out
|
||||
}
|
||||
out = appendClaudeContentBlockStop(out, st)
|
||||
if !st.Completed {
|
||||
out = appendClaudeMessageDelta(out, gjson.Result{}, st)
|
||||
}
|
||||
if !st.Stopped {
|
||||
out = append(out, translatorcommon.AppendSSEEventString(nil, "message_stop", `{"type":"message_stop"}`, 3))
|
||||
st.Stopped = true
|
||||
}
|
||||
st.Done = true
|
||||
return out
|
||||
}
|
||||
|
||||
func setClaudeUsageFromInteractions(out []byte, path string, usage gjson.Result) []byte {
|
||||
if !usage.Exists() {
|
||||
return out
|
||||
}
|
||||
if v, ok := firstUsageInt(usage, "input_tokens", "total_input_tokens"); ok {
|
||||
out, _ = sjson.SetBytes(out, path+".input_tokens", v)
|
||||
}
|
||||
if v, ok := firstUsageInt(usage, "output_tokens", "total_output_tokens"); ok {
|
||||
out, _ = sjson.SetBytes(out, path+".output_tokens", v)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func interactionsSSEPayload(rawJSON []byte) []byte {
|
||||
trimmed := bytes.TrimSpace(rawJSON)
|
||||
if len(trimmed) == 0 || bytes.Equal(trimmed, []byte("[DONE]")) {
|
||||
return trimmed
|
||||
}
|
||||
if bytes.HasPrefix(trimmed, []byte("data:")) {
|
||||
return bytes.TrimSpace(trimmed[len("data:"):])
|
||||
}
|
||||
var dataLines [][]byte
|
||||
for _, line := range bytes.Split(trimmed, []byte("\n")) {
|
||||
line = bytes.TrimSpace(line)
|
||||
if bytes.HasPrefix(line, []byte("data:")) {
|
||||
dataLines = append(dataLines, bytes.TrimSpace(line[len("data:"):]))
|
||||
}
|
||||
}
|
||||
if len(dataLines) > 0 {
|
||||
return bytes.Join(dataLines, []byte("\n"))
|
||||
}
|
||||
return trimmed
|
||||
}
|
||||
|
||||
func interactionsContentTexts(content gjson.Result) []string {
|
||||
if !content.Exists() {
|
||||
return nil
|
||||
}
|
||||
if content.Type == gjson.String {
|
||||
return []string{content.String()}
|
||||
}
|
||||
var out []string
|
||||
content.ForEach(func(_, part gjson.Result) bool {
|
||||
if text := firstNonEmpty(part.Get("text").String(), part.Get("content.text").String()); text != "" {
|
||||
out = append(out, text)
|
||||
}
|
||||
return true
|
||||
})
|
||||
return out
|
||||
}
|
||||
|
||||
func interactionsToolID(root gjson.Result) string {
|
||||
return firstNonEmpty(root.Get("call_id").String(), root.Get("id").String(), root.Get("tool_use_id").String(), "toolu_interactions")
|
||||
}
|
||||
|
||||
func interactionsSignature(root gjson.Result) string {
|
||||
return firstNonEmpty(
|
||||
root.Get("signature").String(),
|
||||
root.Get("thought_signature").String(),
|
||||
root.Get("thoughtSignature").String(),
|
||||
root.Get("extra_content.google.thought_signature").String(),
|
||||
)
|
||||
}
|
||||
|
||||
func firstExisting(root gjson.Result, paths ...string) gjson.Result {
|
||||
for _, path := range paths {
|
||||
if value := root.Get(path); value.Exists() {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return gjson.Result{}
|
||||
}
|
||||
|
||||
func firstUsageInt(root gjson.Result, paths ...string) (int64, bool) {
|
||||
for _, path := range paths {
|
||||
if value := root.Get(path); value.Exists() {
|
||||
return value.Int(), true
|
||||
}
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func firstNonEmpty(values ...string) string {
|
||||
for _, value := range values {
|
||||
if strings.TrimSpace(value) != "" {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (st *interactionsToClaudeStreamState) ensureMaps() {
|
||||
if st.StepTypes == nil {
|
||||
st.StepTypes = 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.ToolSignatures == nil {
|
||||
st.ToolSignatures = make(map[int]string)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,164 @@
|
|||
package claude
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
func TestConvertClaudeRequestToInteractionsMapsMessagesToolsAndStream(t *testing.T) {
|
||||
raw := []byte(`{"model":"gemini-3.1-flash-lite","stream":true,"max_tokens":1024,"tools":[{"name":"get_weather","description":"Weather","input_schema":{"type":"object","properties":{"location":{"type":"string"}},"required":["location"]}}],"messages":[{"role":"user","content":[{"type":"text","text":"今天北京的天气怎么样?"}]}]}`)
|
||||
out := ConvertClaudeRequestToInteractions("gemini-3.1-flash-lite", raw, true)
|
||||
if got := gjson.GetBytes(out, "model").String(); got != "gemini-3.1-flash-lite" {
|
||||
t.Fatalf("model = %q, want gemini-3.1-flash-lite. Output: %s", got, string(out))
|
||||
}
|
||||
if !gjson.GetBytes(out, "stream").Bool() {
|
||||
t.Fatalf("stream should be true. Output: %s", string(out))
|
||||
}
|
||||
if got := gjson.GetBytes(out, "generation_config.max_output_tokens").Int(); got != 1024 {
|
||||
t.Fatalf("max_output_tokens = %d, want 1024. Output: %s", got, string(out))
|
||||
}
|
||||
if got := gjson.GetBytes(out, "input.0.type").String(); got != "user_input" {
|
||||
t.Fatalf("input.0.type = %q, want user_input. Output: %s", got, string(out))
|
||||
}
|
||||
if got := gjson.GetBytes(out, "input.0.content.0.text").String(); got != "今天北京的天气怎么样?" {
|
||||
t.Fatalf("input text = %q. Output: %s", got, string(out))
|
||||
}
|
||||
if got := gjson.GetBytes(out, "tools.0.parameters.properties.location.type").String(); got != "string" {
|
||||
t.Fatalf("tool schema was not mapped. Output: %s", string(out))
|
||||
}
|
||||
if got := gjson.GetBytes(out, "tools.0.type").String(); got != "function" {
|
||||
t.Fatalf("tools.0.type = %q, want function. Output: %s", got, string(out))
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertClaudeRequestToInteractionsMapsToolUseAndResult(t *testing.T) {
|
||||
raw := []byte(`{"model":"gemini-3.1-flash-lite","messages":[{"role":"assistant","content":[{"type":"tool_use","id":"toolu_1","name":"get_weather","input":{"location":"北京"}}]},{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_1","content":"晴"}]}]}`)
|
||||
out := ConvertClaudeRequestToInteractions("gemini-3.1-flash-lite", raw, false)
|
||||
if got := gjson.GetBytes(out, "input.0.type").String(); got != "function_call" {
|
||||
t.Fatalf("input.0.type = %q, want function_call. Output: %s", got, string(out))
|
||||
}
|
||||
if got := gjson.GetBytes(out, "input.0.call_id").String(); got != "toolu_1" {
|
||||
t.Fatalf("call_id = %q, want toolu_1. Output: %s", got, string(out))
|
||||
}
|
||||
if got := gjson.GetBytes(out, "input.1.type").String(); got != "function_result" {
|
||||
t.Fatalf("input.1.type = %q, want function_result. Output: %s", got, string(out))
|
||||
}
|
||||
if got := gjson.GetBytes(out, "input.1.result").String(); got != "晴" {
|
||||
t.Fatalf("result = %q, want 晴. Output: %s", got, string(out))
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertInteractionsResponseToClaudeStream(t *testing.T) {
|
||||
var param any
|
||||
var out [][]byte
|
||||
chunks := [][]byte{
|
||||
[]byte(`event: interaction.created
|
||||
data: {"interaction":{"id":"interaction_1","model":"gemini-3.1-flash-lite"},"event_type":"interaction.created"}`),
|
||||
[]byte(`event: step.start
|
||||
data: {"index":0,"step":{"type":"model_output"},"event_type":"step.start"}`),
|
||||
[]byte(`event: step.delta
|
||||
data: {"index":0,"delta":{"type":"text","text":"北京今天晴"},"event_type":"step.delta"}`),
|
||||
[]byte(`event: step.stop
|
||||
data: {"index":0,"event_type":"step.stop"}`),
|
||||
[]byte(`event: interaction.completed
|
||||
data: {"interaction":{"id":"interaction_1","model":"gemini-3.1-flash-lite","usage":{"total_input_tokens":3,"total_output_tokens":4}},"event_type":"interaction.completed"}`),
|
||||
[]byte(`event: done
|
||||
data: [DONE]`),
|
||||
}
|
||||
for _, chunk := range chunks {
|
||||
out = append(out, ConvertInteractionsResponseToClaude(context.Background(), "gemini-3.1-flash-lite", nil, nil, chunk, ¶m)...)
|
||||
}
|
||||
if payload := findClaudeEventPayload(out, "message_start"); gjson.GetBytes(payload, "message.model").String() != "gemini-3.1-flash-lite" {
|
||||
t.Fatalf("message_start payload = %s", payload)
|
||||
}
|
||||
if payload := findClaudeEventPayload(out, "content_block_delta"); gjson.GetBytes(payload, "delta.text").String() != "北京今天晴" {
|
||||
t.Fatalf("content_block_delta payload = %s", payload)
|
||||
}
|
||||
if payload := findClaudeEventPayload(out, "message_delta"); gjson.GetBytes(payload, "usage.output_tokens").Int() != 4 {
|
||||
t.Fatalf("message_delta payload = %s", payload)
|
||||
}
|
||||
if payload := findClaudeEventPayload(out, "message_stop"); gjson.GetBytes(payload, "type").String() != "message_stop" {
|
||||
t.Fatalf("message_stop payload = %s", payload)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertInteractionsResponseToClaudeStreamToolCall(t *testing.T) {
|
||||
var param any
|
||||
var out [][]byte
|
||||
chunks := [][]byte{
|
||||
[]byte(`data: {"interaction":{"id":"interaction_1","model":"gemini-3.1-flash-lite"},"event_type":"interaction.created"}`),
|
||||
[]byte(`data: {"index":0,"step":{"type":"function_call","id":"toolu_1","signature":"sig_1","name":"get_weather","arguments":{}},"event_type":"step.start"}`),
|
||||
[]byte(`data: {"index":0,"delta":{"type":"arguments_delta","arguments":"{\"location\":\"北京\"}"},"event_type":"step.delta"}`),
|
||||
[]byte(`data: {"index":0,"event_type":"step.stop"}`),
|
||||
[]byte(`data: {"interaction":{"usage":{"total_input_tokens":1,"total_output_tokens":2}},"event_type":"interaction.completed"}`),
|
||||
}
|
||||
for _, chunk := range chunks {
|
||||
out = append(out, ConvertInteractionsResponseToClaude(context.Background(), "gemini-3.1-flash-lite", nil, nil, chunk, ¶m)...)
|
||||
}
|
||||
if payload := findClaudeEventPayload(out, "content_block_start"); gjson.GetBytes(payload, "content_block.type").String() != "tool_use" {
|
||||
t.Fatalf("content_block_start payload = %s", payload)
|
||||
}
|
||||
if payload := findClaudeEventPayload(out, "content_block_start"); gjson.GetBytes(payload, "content_block.signature").String() != "sig_1" {
|
||||
t.Fatalf("content_block_start signature payload = %s", payload)
|
||||
}
|
||||
if payload := findClaudeEventPayload(out, "content_block_delta"); gjson.GetBytes(payload, "delta.partial_json").String() != `{"location":"北京"}` {
|
||||
t.Fatalf("content_block_delta payload = %s", payload)
|
||||
}
|
||||
if payload := findClaudeEventPayload(out, "message_delta"); gjson.GetBytes(payload, "delta.stop_reason").String() != "tool_use" {
|
||||
t.Fatalf("message_delta payload = %s", payload)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertInteractionsResponseToClaudeStreamFinishMetadataUsage(t *testing.T) {
|
||||
var param any
|
||||
out := ConvertInteractionsResponseToClaude(context.Background(), "claude-test", nil, nil, []byte(`data: {"event_type":"finish","metadata":{"total_usage":{"total_input_tokens":2,"total_output_tokens":6,"total_tokens":8}}}`), ¶m)
|
||||
payload := findClaudeEventPayload(out, "message_delta")
|
||||
if len(payload) == 0 {
|
||||
t.Fatalf("message_delta payload not found")
|
||||
}
|
||||
if got := gjson.GetBytes(payload, "usage.input_tokens").Int(); got != 2 {
|
||||
t.Fatalf("input_tokens = %d, want 2. Payload: %s", got, string(payload))
|
||||
}
|
||||
if got := gjson.GetBytes(payload, "usage.output_tokens").Int(); got != 6 {
|
||||
t.Fatalf("output_tokens = %d, want 6. Payload: %s", got, string(payload))
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertInteractionsResponseToClaudeNonStream(t *testing.T) {
|
||||
raw := []byte(`{"id":"interaction_1","model":"gemini-3.1-flash-lite","steps":[{"type":"model_output","content":[{"type":"text","text":"ok"}]},{"type":"function_call","call_id":"toolu_1","signature":"sig_1","name":"lookup","arguments":{"q":"x"}}],"usage":{"total_input_tokens":3,"total_output_tokens":4}}`)
|
||||
out := ConvertInteractionsResponseToClaudeNonStream(context.Background(), "gemini-3.1-flash-lite", nil, nil, raw, nil)
|
||||
if got := gjson.GetBytes(out, "content.0.text").String(); got != "ok" {
|
||||
t.Fatalf("text = %q, want ok. Output: %s", got, string(out))
|
||||
}
|
||||
if got := gjson.GetBytes(out, "content.1.type").String(); got != "tool_use" {
|
||||
t.Fatalf("tool block type = %q, want tool_use. Output: %s", got, string(out))
|
||||
}
|
||||
if got := gjson.GetBytes(out, "content.1.signature").String(); got != "sig_1" {
|
||||
t.Fatalf("tool signature = %q, want sig_1. Output: %s", got, string(out))
|
||||
}
|
||||
if got := gjson.GetBytes(out, "stop_reason").String(); got != "tool_use" {
|
||||
t.Fatalf("stop_reason = %q, want tool_use. Output: %s", got, string(out))
|
||||
}
|
||||
if got := gjson.GetBytes(out, "usage.input_tokens").Int(); got != 3 {
|
||||
t.Fatalf("input_tokens = %d, want 3. Output: %s", got, string(out))
|
||||
}
|
||||
}
|
||||
|
||||
func findClaudeEventPayload(events [][]byte, eventName string) []byte {
|
||||
prefix := []byte("data:")
|
||||
for _, event := range events {
|
||||
if !bytes.Contains(event, []byte("event: "+eventName)) {
|
||||
continue
|
||||
}
|
||||
for _, line := range bytes.Split(event, []byte("\n")) {
|
||||
line = bytes.TrimSpace(line)
|
||||
if bytes.HasPrefix(line, prefix) {
|
||||
return bytes.TrimSpace(line[len(prefix):])
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
package interactions_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestInteractionsTranslatorsDoNotImportGeminiTranslators(t *testing.T) {
|
||||
repoRoot := filepath.Clean(filepath.Join("..", "..", ".."))
|
||||
scanDirs := []string{
|
||||
"internal/translator/openai/interactions",
|
||||
"internal/translator/claude/interactions",
|
||||
"internal/translator/codex/interactions",
|
||||
"internal/translator/antigravity/interactions",
|
||||
}
|
||||
forbidden := regexp.MustCompile(`"github\.com/router-for-me/CLIProxyAPI/v7/internal/translator/[^"]*/gemini[^"]*"`)
|
||||
var violations []string
|
||||
for _, scanDir := range scanDirs {
|
||||
root := filepath.Join(repoRoot, scanDir)
|
||||
errWalk := filepath.WalkDir(root, func(path string, entry os.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if entry.IsDir() || !strings.HasSuffix(path, ".go") {
|
||||
return nil
|
||||
}
|
||||
data, errRead := os.ReadFile(path)
|
||||
if errRead != nil {
|
||||
return errRead
|
||||
}
|
||||
if forbidden.Match(data) {
|
||||
rel, errRel := filepath.Rel(repoRoot, path)
|
||||
if errRel != nil {
|
||||
rel = path
|
||||
}
|
||||
violations = append(violations, rel)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if errWalk != nil {
|
||||
t.Fatalf("scan %s: %v", scanDir, errWalk)
|
||||
}
|
||||
}
|
||||
if len(violations) > 0 {
|
||||
t.Fatalf("non-Gemini Interactions translators import Gemini translators: %s", strings.Join(violations, ", "))
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue