mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-03 04:50:52 +08:00
refactor: replace json.Marshal and json.Unmarshal with sjson and gjson
Optimized the handling of JSON serialization and deserialization by replacing redundant `json.Marshal` and `json.Unmarshal` calls with `sjson` and `gjson`. Introduced a `marshalJSONValue` utility for compact JSON encoding, improving performance and code simplicity. Removed unused `encoding/json` imports.
This commit is contained in:
@@ -452,7 +452,7 @@ func ConvertAntigravityResponseToClaudeNonStream(_ context.Context, _ string, or
|
|||||||
toolBlock, _ = sjson.Set(toolBlock, "id", fmt.Sprintf("tool_%d", toolIDCounter))
|
toolBlock, _ = sjson.Set(toolBlock, "id", fmt.Sprintf("tool_%d", toolIDCounter))
|
||||||
toolBlock, _ = sjson.Set(toolBlock, "name", name)
|
toolBlock, _ = sjson.Set(toolBlock, "name", name)
|
||||||
|
|
||||||
if args := functionCall.Get("args"); args.Exists() && args.Raw != "" && gjson.Valid(args.Raw) {
|
if args := functionCall.Get("args"); args.Exists() && args.Raw != "" && gjson.Valid(args.Raw) && args.IsObject() {
|
||||||
toolBlock, _ = sjson.SetRaw(toolBlock, "input", args.Raw)
|
toolBlock, _ = sjson.SetRaw(toolBlock, "input", args.Raw)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ package gemini
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini/common"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini/common"
|
||||||
@@ -135,41 +134,31 @@ func ConvertGeminiRequestToAntigravity(_ string, inputRawJSON []byte, _ bool) []
|
|||||||
|
|
||||||
// FunctionCallGroup represents a group of function calls and their responses
|
// FunctionCallGroup represents a group of function calls and their responses
|
||||||
type FunctionCallGroup struct {
|
type FunctionCallGroup struct {
|
||||||
ModelContent map[string]interface{}
|
|
||||||
FunctionCalls []gjson.Result
|
|
||||||
ResponsesNeeded int
|
ResponsesNeeded int
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseFunctionResponse attempts to unmarshal a function response part.
|
// parseFunctionResponseRaw attempts to normalize a function response part into a JSON object string.
|
||||||
// Falls back to gjson extraction if standard json.Unmarshal fails.
|
// Falls back to a minimal "functionResponse" object when parsing fails.
|
||||||
func parseFunctionResponse(response gjson.Result) map[string]interface{} {
|
func parseFunctionResponseRaw(response gjson.Result) string {
|
||||||
var responseMap map[string]interface{}
|
if response.IsObject() && gjson.Valid(response.Raw) {
|
||||||
err := json.Unmarshal([]byte(response.Raw), &responseMap)
|
return response.Raw
|
||||||
if err == nil {
|
|
||||||
return responseMap
|
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debugf("unmarshal function response failed, using fallback: %v", err)
|
log.Debugf("parse function response failed, using fallback")
|
||||||
funcResp := response.Get("functionResponse")
|
funcResp := response.Get("functionResponse")
|
||||||
if funcResp.Exists() {
|
if funcResp.Exists() {
|
||||||
fr := map[string]interface{}{
|
fr := `{"functionResponse":{"name":"","response":{"result":""}}}`
|
||||||
"name": funcResp.Get("name").String(),
|
fr, _ = sjson.Set(fr, "functionResponse.name", funcResp.Get("name").String())
|
||||||
"response": map[string]interface{}{
|
fr, _ = sjson.Set(fr, "functionResponse.response.result", funcResp.Get("response").String())
|
||||||
"result": funcResp.Get("response").String(),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
if id := funcResp.Get("id").String(); id != "" {
|
if id := funcResp.Get("id").String(); id != "" {
|
||||||
fr["id"] = id
|
fr, _ = sjson.Set(fr, "functionResponse.id", id)
|
||||||
}
|
}
|
||||||
return map[string]interface{}{"functionResponse": fr}
|
return fr
|
||||||
}
|
}
|
||||||
|
|
||||||
return map[string]interface{}{
|
fr := `{"functionResponse":{"name":"unknown","response":{"result":""}}}`
|
||||||
"functionResponse": map[string]interface{}{
|
fr, _ = sjson.Set(fr, "functionResponse.response.result", response.String())
|
||||||
"name": "unknown",
|
return fr
|
||||||
"response": map[string]interface{}{"result": response.String()},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// fixCLIToolResponse performs sophisticated tool response format conversion and grouping.
|
// fixCLIToolResponse performs sophisticated tool response format conversion and grouping.
|
||||||
@@ -196,7 +185,7 @@ func fixCLIToolResponse(input string) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Initialize data structures for processing and grouping
|
// Initialize data structures for processing and grouping
|
||||||
var newContents []interface{} // Final processed contents array
|
contentsWrapper := `{"contents":[]}`
|
||||||
var pendingGroups []*FunctionCallGroup // Groups awaiting completion with responses
|
var pendingGroups []*FunctionCallGroup // Groups awaiting completion with responses
|
||||||
var collectedResponses []gjson.Result // Standalone responses to be matched
|
var collectedResponses []gjson.Result // Standalone responses to be matched
|
||||||
|
|
||||||
@@ -228,17 +217,16 @@ func fixCLIToolResponse(input string) (string, error) {
|
|||||||
collectedResponses = collectedResponses[group.ResponsesNeeded:]
|
collectedResponses = collectedResponses[group.ResponsesNeeded:]
|
||||||
|
|
||||||
// Create merged function response content
|
// Create merged function response content
|
||||||
var responseParts []interface{}
|
functionResponseContent := `{"parts":[],"role":"function"}`
|
||||||
for _, response := range groupResponses {
|
for _, response := range groupResponses {
|
||||||
responseParts = append(responseParts, parseFunctionResponse(response))
|
partRaw := parseFunctionResponseRaw(response)
|
||||||
|
if partRaw != "" {
|
||||||
|
functionResponseContent, _ = sjson.SetRaw(functionResponseContent, "parts.-1", partRaw)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(responseParts) > 0 {
|
if gjson.Get(functionResponseContent, "parts.#").Int() > 0 {
|
||||||
functionResponseContent := map[string]interface{}{
|
contentsWrapper, _ = sjson.SetRaw(contentsWrapper, "contents.-1", functionResponseContent)
|
||||||
"parts": responseParts,
|
|
||||||
"role": "function",
|
|
||||||
}
|
|
||||||
newContents = append(newContents, functionResponseContent)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove this group as it's been satisfied
|
// Remove this group as it's been satisfied
|
||||||
@@ -252,50 +240,42 @@ func fixCLIToolResponse(input string) (string, error) {
|
|||||||
|
|
||||||
// If this is a model with function calls, create a new group
|
// If this is a model with function calls, create a new group
|
||||||
if role == "model" {
|
if role == "model" {
|
||||||
var functionCallsInThisModel []gjson.Result
|
functionCallsCount := 0
|
||||||
parts.ForEach(func(_, part gjson.Result) bool {
|
parts.ForEach(func(_, part gjson.Result) bool {
|
||||||
if part.Get("functionCall").Exists() {
|
if part.Get("functionCall").Exists() {
|
||||||
functionCallsInThisModel = append(functionCallsInThisModel, part)
|
functionCallsCount++
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
|
|
||||||
if len(functionCallsInThisModel) > 0 {
|
if functionCallsCount > 0 {
|
||||||
// Add the model content
|
// Add the model content
|
||||||
var contentMap map[string]interface{}
|
if !value.IsObject() {
|
||||||
errUnmarshal := json.Unmarshal([]byte(value.Raw), &contentMap)
|
log.Warnf("failed to parse model content")
|
||||||
if errUnmarshal != nil {
|
|
||||||
log.Warnf("failed to unmarshal model content: %v\n", errUnmarshal)
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
newContents = append(newContents, contentMap)
|
contentsWrapper, _ = sjson.SetRaw(contentsWrapper, "contents.-1", value.Raw)
|
||||||
|
|
||||||
// Create a new group for tracking responses
|
// Create a new group for tracking responses
|
||||||
group := &FunctionCallGroup{
|
group := &FunctionCallGroup{
|
||||||
ModelContent: contentMap,
|
ResponsesNeeded: functionCallsCount,
|
||||||
FunctionCalls: functionCallsInThisModel,
|
|
||||||
ResponsesNeeded: len(functionCallsInThisModel),
|
|
||||||
}
|
}
|
||||||
pendingGroups = append(pendingGroups, group)
|
pendingGroups = append(pendingGroups, group)
|
||||||
} else {
|
} else {
|
||||||
// Regular model content without function calls
|
// Regular model content without function calls
|
||||||
var contentMap map[string]interface{}
|
if !value.IsObject() {
|
||||||
errUnmarshal := json.Unmarshal([]byte(value.Raw), &contentMap)
|
log.Warnf("failed to parse content")
|
||||||
if errUnmarshal != nil {
|
|
||||||
log.Warnf("failed to unmarshal content: %v\n", errUnmarshal)
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
newContents = append(newContents, contentMap)
|
contentsWrapper, _ = sjson.SetRaw(contentsWrapper, "contents.-1", value.Raw)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Non-model content (user, etc.)
|
// Non-model content (user, etc.)
|
||||||
var contentMap map[string]interface{}
|
if !value.IsObject() {
|
||||||
errUnmarshal := json.Unmarshal([]byte(value.Raw), &contentMap)
|
log.Warnf("failed to parse content")
|
||||||
if errUnmarshal != nil {
|
|
||||||
log.Warnf("failed to unmarshal content: %v\n", errUnmarshal)
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
newContents = append(newContents, contentMap)
|
contentsWrapper, _ = sjson.SetRaw(contentsWrapper, "contents.-1", value.Raw)
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
@@ -307,25 +287,23 @@ func fixCLIToolResponse(input string) (string, error) {
|
|||||||
groupResponses := collectedResponses[:group.ResponsesNeeded]
|
groupResponses := collectedResponses[:group.ResponsesNeeded]
|
||||||
collectedResponses = collectedResponses[group.ResponsesNeeded:]
|
collectedResponses = collectedResponses[group.ResponsesNeeded:]
|
||||||
|
|
||||||
var responseParts []interface{}
|
functionResponseContent := `{"parts":[],"role":"function"}`
|
||||||
for _, response := range groupResponses {
|
for _, response := range groupResponses {
|
||||||
responseParts = append(responseParts, parseFunctionResponse(response))
|
partRaw := parseFunctionResponseRaw(response)
|
||||||
|
if partRaw != "" {
|
||||||
|
functionResponseContent, _ = sjson.SetRaw(functionResponseContent, "parts.-1", partRaw)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(responseParts) > 0 {
|
if gjson.Get(functionResponseContent, "parts.#").Int() > 0 {
|
||||||
functionResponseContent := map[string]interface{}{
|
contentsWrapper, _ = sjson.SetRaw(contentsWrapper, "contents.-1", functionResponseContent)
|
||||||
"parts": responseParts,
|
|
||||||
"role": "function",
|
|
||||||
}
|
|
||||||
newContents = append(newContents, functionResponseContent)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the original JSON with the new contents
|
// Update the original JSON with the new contents
|
||||||
result := input
|
result := input
|
||||||
newContentsJSON, _ := json.Marshal(newContents)
|
result, _ = sjson.SetRaw(result, "request.contents", gjson.Get(contentsWrapper, "contents").Raw)
|
||||||
result, _ = sjson.Set(result, "request.contents", json.RawMessage(newContentsJSON))
|
|
||||||
|
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -319,7 +319,7 @@ func ConvertOpenAIRequestToAntigravity(modelName string, inputRawJSON []byte, _
|
|||||||
log.Warnf("Failed to set default schema type for tool '%s': %v", fn.Get("name").String(), errSet)
|
log.Warnf("Failed to set default schema type for tool '%s': %v", fn.Get("name").String(), errSet)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
fnRaw, errSet = sjson.Set(fnRaw, "parametersJsonSchema.properties", map[string]interface{}{})
|
fnRaw, errSet = sjson.SetRaw(fnRaw, "parametersJsonSchema.properties", `{}`)
|
||||||
if errSet != nil {
|
if errSet != nil {
|
||||||
log.Warnf("Failed to set default schema properties for tool '%s': %v", fn.Get("name").String(), errSet)
|
log.Warnf("Failed to set default schema properties for tool '%s': %v", fn.Get("name").String(), errSet)
|
||||||
continue
|
continue
|
||||||
@@ -334,7 +334,7 @@ func ConvertOpenAIRequestToAntigravity(modelName string, inputRawJSON []byte, _
|
|||||||
log.Warnf("Failed to set default schema type for tool '%s': %v", fn.Get("name").String(), errSet)
|
log.Warnf("Failed to set default schema type for tool '%s': %v", fn.Get("name").String(), errSet)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
fnRaw, errSet = sjson.Set(fnRaw, "parametersJsonSchema.properties", map[string]interface{}{})
|
fnRaw, errSet = sjson.SetRaw(fnRaw, "parametersJsonSchema.properties", `{}`)
|
||||||
if errSet != nil {
|
if errSet != nil {
|
||||||
log.Warnf("Failed to set default schema properties for tool '%s': %v", fn.Get("name").String(), errSet)
|
log.Warnf("Failed to set default schema properties for tool '%s': %v", fn.Get("name").String(), errSet)
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ package chat_completions
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
@@ -171,21 +170,14 @@ func ConvertAntigravityResponseToOpenAI(_ context.Context, _ string, originalReq
|
|||||||
mimeType = "image/png"
|
mimeType = "image/png"
|
||||||
}
|
}
|
||||||
imageURL := fmt.Sprintf("data:%s;base64,%s", mimeType, data)
|
imageURL := fmt.Sprintf("data:%s;base64,%s", mimeType, data)
|
||||||
imagePayload, err := json.Marshal(map[string]any{
|
imagePayload := `{"image_url":{"url":""},"type":"image_url"}`
|
||||||
"type": "image_url",
|
imagePayload, _ = sjson.Set(imagePayload, "image_url.url", imageURL)
|
||||||
"image_url": map[string]string{
|
|
||||||
"url": imageURL,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
imagesResult := gjson.Get(template, "choices.0.delta.images")
|
imagesResult := gjson.Get(template, "choices.0.delta.images")
|
||||||
if !imagesResult.Exists() || !imagesResult.IsArray() {
|
if !imagesResult.Exists() || !imagesResult.IsArray() {
|
||||||
template, _ = sjson.SetRaw(template, "choices.0.delta.images", `[]`)
|
template, _ = sjson.SetRaw(template, "choices.0.delta.images", `[]`)
|
||||||
}
|
}
|
||||||
template, _ = sjson.Set(template, "choices.0.delta.role", "assistant")
|
template, _ = sjson.Set(template, "choices.0.delta.role", "assistant")
|
||||||
template, _ = sjson.SetRaw(template, "choices.0.delta.images.-1", string(imagePayload))
|
template, _ = sjson.SetRaw(template, "choices.0.delta.images.-1", imagePayload)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -194,7 +194,7 @@ func ConvertGeminiRequestToClaude(modelName string, inputRawJSON []byte, stream
|
|||||||
if name := fc.Get("name"); name.Exists() {
|
if name := fc.Get("name"); name.Exists() {
|
||||||
toolUse, _ = sjson.Set(toolUse, "name", name.String())
|
toolUse, _ = sjson.Set(toolUse, "name", name.String())
|
||||||
}
|
}
|
||||||
if args := fc.Get("args"); args.Exists() {
|
if args := fc.Get("args"); args.Exists() && args.IsObject() {
|
||||||
toolUse, _ = sjson.SetRaw(toolUse, "input", args.Raw)
|
toolUse, _ = sjson.SetRaw(toolUse, "input", args.Raw)
|
||||||
}
|
}
|
||||||
msg, _ = sjson.SetRaw(msg, "content.-1", toolUse)
|
msg, _ = sjson.SetRaw(msg, "content.-1", toolUse)
|
||||||
@@ -314,11 +314,11 @@ func ConvertGeminiRequestToClaude(modelName string, inputRawJSON []byte, stream
|
|||||||
if mode := funcCalling.Get("mode"); mode.Exists() {
|
if mode := funcCalling.Get("mode"); mode.Exists() {
|
||||||
switch mode.String() {
|
switch mode.String() {
|
||||||
case "AUTO":
|
case "AUTO":
|
||||||
out, _ = sjson.Set(out, "tool_choice", map[string]interface{}{"type": "auto"})
|
out, _ = sjson.SetRaw(out, "tool_choice", `{"type":"auto"}`)
|
||||||
case "NONE":
|
case "NONE":
|
||||||
out, _ = sjson.Set(out, "tool_choice", map[string]interface{}{"type": "none"})
|
out, _ = sjson.SetRaw(out, "tool_choice", `{"type":"none"}`)
|
||||||
case "ANY":
|
case "ANY":
|
||||||
out, _ = sjson.Set(out, "tool_choice", map[string]interface{}{"type": "any"})
|
out, _ = sjson.SetRaw(out, "tool_choice", `{"type":"any"}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -263,51 +263,6 @@ func ConvertClaudeResponseToGemini(_ context.Context, modelName string, original
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// convertArrayToJSON converts []interface{} to JSON array string
|
|
||||||
func convertArrayToJSON(arr []interface{}) string {
|
|
||||||
result := "[]"
|
|
||||||
for _, item := range arr {
|
|
||||||
switch itemData := item.(type) {
|
|
||||||
case map[string]interface{}:
|
|
||||||
itemJSON := convertMapToJSON(itemData)
|
|
||||||
result, _ = sjson.SetRaw(result, "-1", itemJSON)
|
|
||||||
case string:
|
|
||||||
result, _ = sjson.Set(result, "-1", itemData)
|
|
||||||
case bool:
|
|
||||||
result, _ = sjson.Set(result, "-1", itemData)
|
|
||||||
case float64, int, int64:
|
|
||||||
result, _ = sjson.Set(result, "-1", itemData)
|
|
||||||
default:
|
|
||||||
result, _ = sjson.Set(result, "-1", itemData)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// convertMapToJSON converts map[string]interface{} to JSON object string
|
|
||||||
func convertMapToJSON(m map[string]interface{}) string {
|
|
||||||
result := "{}"
|
|
||||||
for key, value := range m {
|
|
||||||
switch val := value.(type) {
|
|
||||||
case map[string]interface{}:
|
|
||||||
nestedJSON := convertMapToJSON(val)
|
|
||||||
result, _ = sjson.SetRaw(result, key, nestedJSON)
|
|
||||||
case []interface{}:
|
|
||||||
arrayJSON := convertArrayToJSON(val)
|
|
||||||
result, _ = sjson.SetRaw(result, key, arrayJSON)
|
|
||||||
case string:
|
|
||||||
result, _ = sjson.Set(result, key, val)
|
|
||||||
case bool:
|
|
||||||
result, _ = sjson.Set(result, key, val)
|
|
||||||
case float64, int, int64:
|
|
||||||
result, _ = sjson.Set(result, key, val)
|
|
||||||
default:
|
|
||||||
result, _ = sjson.Set(result, key, val)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// ConvertClaudeResponseToGeminiNonStream converts a non-streaming Claude Code response to a non-streaming Gemini response.
|
// ConvertClaudeResponseToGeminiNonStream converts a non-streaming Claude Code response to a non-streaming Gemini response.
|
||||||
// This function processes the complete Claude Code response and transforms it into a single Gemini-compatible
|
// This function processes the complete Claude Code response and transforms it into a single Gemini-compatible
|
||||||
// JSON response. It handles message content, tool calls, reasoning content, and usage metadata, combining all
|
// JSON response. It handles message content, tool calls, reasoning content, and usage metadata, combining all
|
||||||
@@ -356,8 +311,8 @@ func ConvertClaudeResponseToGeminiNonStream(_ context.Context, modelName string,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Process each streaming event and collect parts
|
// Process each streaming event and collect parts
|
||||||
var allParts []interface{}
|
var allParts []string
|
||||||
var finalUsage map[string]interface{}
|
var finalUsageJSON string
|
||||||
var responseID string
|
var responseID string
|
||||||
var createdAt int64
|
var createdAt int64
|
||||||
|
|
||||||
@@ -407,16 +362,14 @@ func ConvertClaudeResponseToGeminiNonStream(_ context.Context, modelName string,
|
|||||||
if text := delta.Get("text"); text.Exists() && text.String() != "" {
|
if text := delta.Get("text"); text.Exists() && text.String() != "" {
|
||||||
partJSON := `{"text":""}`
|
partJSON := `{"text":""}`
|
||||||
partJSON, _ = sjson.Set(partJSON, "text", text.String())
|
partJSON, _ = sjson.Set(partJSON, "text", text.String())
|
||||||
part := gjson.Parse(partJSON).Value().(map[string]interface{})
|
allParts = append(allParts, partJSON)
|
||||||
allParts = append(allParts, part)
|
|
||||||
}
|
}
|
||||||
case "thinking_delta":
|
case "thinking_delta":
|
||||||
// Process reasoning/thinking content
|
// Process reasoning/thinking content
|
||||||
if text := delta.Get("thinking"); text.Exists() && text.String() != "" {
|
if text := delta.Get("thinking"); text.Exists() && text.String() != "" {
|
||||||
partJSON := `{"thought":true,"text":""}`
|
partJSON := `{"thought":true,"text":""}`
|
||||||
partJSON, _ = sjson.Set(partJSON, "text", text.String())
|
partJSON, _ = sjson.Set(partJSON, "text", text.String())
|
||||||
part := gjson.Parse(partJSON).Value().(map[string]interface{})
|
allParts = append(allParts, partJSON)
|
||||||
allParts = append(allParts, part)
|
|
||||||
}
|
}
|
||||||
case "input_json_delta":
|
case "input_json_delta":
|
||||||
// accumulate args partial_json for this index
|
// accumulate args partial_json for this index
|
||||||
@@ -456,9 +409,7 @@ func ConvertClaudeResponseToGeminiNonStream(_ context.Context, modelName string,
|
|||||||
if argsTrim != "" {
|
if argsTrim != "" {
|
||||||
functionCallJSON, _ = sjson.SetRaw(functionCallJSON, "functionCall.args", argsTrim)
|
functionCallJSON, _ = sjson.SetRaw(functionCallJSON, "functionCall.args", argsTrim)
|
||||||
}
|
}
|
||||||
// Parse back to interface{} for allParts
|
allParts = append(allParts, functionCallJSON)
|
||||||
functionCall := gjson.Parse(functionCallJSON).Value().(map[string]interface{})
|
|
||||||
allParts = append(allParts, functionCall)
|
|
||||||
// cleanup used state for this index
|
// cleanup used state for this index
|
||||||
if newParam.ToolUseArgs != nil {
|
if newParam.ToolUseArgs != nil {
|
||||||
delete(newParam.ToolUseArgs, idx)
|
delete(newParam.ToolUseArgs, idx)
|
||||||
@@ -501,8 +452,7 @@ func ConvertClaudeResponseToGeminiNonStream(_ context.Context, modelName string,
|
|||||||
// Set traffic type (required by Gemini API)
|
// Set traffic type (required by Gemini API)
|
||||||
usageJSON, _ = sjson.Set(usageJSON, "trafficType", "PROVISIONED_THROUGHPUT")
|
usageJSON, _ = sjson.Set(usageJSON, "trafficType", "PROVISIONED_THROUGHPUT")
|
||||||
|
|
||||||
// Convert to map[string]interface{} using gjson
|
finalUsageJSON = usageJSON
|
||||||
finalUsage = gjson.Parse(usageJSON).Value().(map[string]interface{})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -520,12 +470,16 @@ func ConvertClaudeResponseToGeminiNonStream(_ context.Context, modelName string,
|
|||||||
|
|
||||||
// Set the consolidated parts array
|
// Set the consolidated parts array
|
||||||
if len(consolidatedParts) > 0 {
|
if len(consolidatedParts) > 0 {
|
||||||
template, _ = sjson.SetRaw(template, "candidates.0.content.parts", convertToJSONString(consolidatedParts))
|
partsJSON := "[]"
|
||||||
|
for _, partJSON := range consolidatedParts {
|
||||||
|
partsJSON, _ = sjson.SetRaw(partsJSON, "-1", partJSON)
|
||||||
|
}
|
||||||
|
template, _ = sjson.SetRaw(template, "candidates.0.content.parts", partsJSON)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set usage metadata
|
// Set usage metadata
|
||||||
if finalUsage != nil {
|
if finalUsageJSON != "" {
|
||||||
template, _ = sjson.SetRaw(template, "usageMetadata", convertToJSONString(finalUsage))
|
template, _ = sjson.SetRaw(template, "usageMetadata", finalUsageJSON)
|
||||||
}
|
}
|
||||||
|
|
||||||
return template
|
return template
|
||||||
@@ -539,12 +493,12 @@ func GeminiTokenCount(ctx context.Context, count int64) string {
|
|||||||
// This function processes the parts array to combine adjacent text elements and thinking elements
|
// This function processes the parts array to combine adjacent text elements and thinking elements
|
||||||
// into single consolidated parts, which results in a more readable and efficient response structure.
|
// into single consolidated parts, which results in a more readable and efficient response structure.
|
||||||
// Tool calls and other non-text parts are preserved as separate elements.
|
// Tool calls and other non-text parts are preserved as separate elements.
|
||||||
func consolidateParts(parts []interface{}) []interface{} {
|
func consolidateParts(parts []string) []string {
|
||||||
if len(parts) == 0 {
|
if len(parts) == 0 {
|
||||||
return parts
|
return parts
|
||||||
}
|
}
|
||||||
|
|
||||||
var consolidated []interface{}
|
var consolidated []string
|
||||||
var currentTextPart strings.Builder
|
var currentTextPart strings.Builder
|
||||||
var currentThoughtPart strings.Builder
|
var currentThoughtPart strings.Builder
|
||||||
var hasText, hasThought bool
|
var hasText, hasThought bool
|
||||||
@@ -554,8 +508,7 @@ func consolidateParts(parts []interface{}) []interface{} {
|
|||||||
if hasText && currentTextPart.Len() > 0 {
|
if hasText && currentTextPart.Len() > 0 {
|
||||||
textPartJSON := `{"text":""}`
|
textPartJSON := `{"text":""}`
|
||||||
textPartJSON, _ = sjson.Set(textPartJSON, "text", currentTextPart.String())
|
textPartJSON, _ = sjson.Set(textPartJSON, "text", currentTextPart.String())
|
||||||
textPart := gjson.Parse(textPartJSON).Value().(map[string]interface{})
|
consolidated = append(consolidated, textPartJSON)
|
||||||
consolidated = append(consolidated, textPart)
|
|
||||||
currentTextPart.Reset()
|
currentTextPart.Reset()
|
||||||
hasText = false
|
hasText = false
|
||||||
}
|
}
|
||||||
@@ -566,42 +519,42 @@ func consolidateParts(parts []interface{}) []interface{} {
|
|||||||
if hasThought && currentThoughtPart.Len() > 0 {
|
if hasThought && currentThoughtPart.Len() > 0 {
|
||||||
thoughtPartJSON := `{"thought":true,"text":""}`
|
thoughtPartJSON := `{"thought":true,"text":""}`
|
||||||
thoughtPartJSON, _ = sjson.Set(thoughtPartJSON, "text", currentThoughtPart.String())
|
thoughtPartJSON, _ = sjson.Set(thoughtPartJSON, "text", currentThoughtPart.String())
|
||||||
thoughtPart := gjson.Parse(thoughtPartJSON).Value().(map[string]interface{})
|
consolidated = append(consolidated, thoughtPartJSON)
|
||||||
consolidated = append(consolidated, thoughtPart)
|
|
||||||
currentThoughtPart.Reset()
|
currentThoughtPart.Reset()
|
||||||
hasThought = false
|
hasThought = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, part := range parts {
|
for _, partJSON := range parts {
|
||||||
partMap, ok := part.(map[string]interface{})
|
part := gjson.Parse(partJSON)
|
||||||
if !ok {
|
if !part.Exists() || !part.IsObject() {
|
||||||
// Flush any pending parts and add this non-text part
|
// Flush any pending parts and add this non-text part
|
||||||
flushText()
|
flushText()
|
||||||
flushThought()
|
flushThought()
|
||||||
consolidated = append(consolidated, part)
|
consolidated = append(consolidated, partJSON)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if thought, isThought := partMap["thought"]; isThought && thought == true {
|
thought := part.Get("thought")
|
||||||
|
if thought.Exists() && thought.Type == gjson.True {
|
||||||
// This is a thinking part - flush any pending text first
|
// This is a thinking part - flush any pending text first
|
||||||
flushText() // Flush any pending text first
|
flushText() // Flush any pending text first
|
||||||
|
|
||||||
if text, hasTextContent := partMap["text"].(string); hasTextContent {
|
if text := part.Get("text"); text.Exists() && text.Type == gjson.String {
|
||||||
currentThoughtPart.WriteString(text)
|
currentThoughtPart.WriteString(text.String())
|
||||||
hasThought = true
|
hasThought = true
|
||||||
}
|
}
|
||||||
} else if text, hasTextContent := partMap["text"].(string); hasTextContent {
|
} else if text := part.Get("text"); text.Exists() && text.Type == gjson.String {
|
||||||
// This is a regular text part - flush any pending thought first
|
// This is a regular text part - flush any pending thought first
|
||||||
flushThought() // Flush any pending thought first
|
flushThought() // Flush any pending thought first
|
||||||
|
|
||||||
currentTextPart.WriteString(text)
|
currentTextPart.WriteString(text.String())
|
||||||
hasText = true
|
hasText = true
|
||||||
} else {
|
} else {
|
||||||
// This is some other type of part (like function call) - flush both text and thought
|
// This is some other type of part (like function call) - flush both text and thought
|
||||||
flushText()
|
flushText()
|
||||||
flushThought()
|
flushThought()
|
||||||
consolidated = append(consolidated, part)
|
consolidated = append(consolidated, partJSON)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -611,20 +564,3 @@ func consolidateParts(parts []interface{}) []interface{} {
|
|||||||
|
|
||||||
return consolidated
|
return consolidated
|
||||||
}
|
}
|
||||||
|
|
||||||
// convertToJSONString converts interface{} to JSON string using sjson/gjson.
|
|
||||||
// This function provides a consistent way to serialize different data types to JSON strings
|
|
||||||
// for inclusion in the Gemini API response structure.
|
|
||||||
func convertToJSONString(v interface{}) string {
|
|
||||||
switch val := v.(type) {
|
|
||||||
case []interface{}:
|
|
||||||
return convertArrayToJSON(val)
|
|
||||||
case map[string]interface{}:
|
|
||||||
return convertMapToJSON(val)
|
|
||||||
default:
|
|
||||||
// For simple types, create a temporary JSON and extract the value
|
|
||||||
temp := `{"temp":null}`
|
|
||||||
temp, _ = sjson.Set(temp, "temp", val)
|
|
||||||
return gjson.Get(temp, "temp").Raw
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import (
|
|||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"math/big"
|
"math/big"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -137,9 +136,6 @@ func ConvertOpenAIRequestToClaude(modelName string, inputRawJSON []byte, stream
|
|||||||
out, _ = sjson.Set(out, "stream", stream)
|
out, _ = sjson.Set(out, "stream", stream)
|
||||||
|
|
||||||
// Process messages and transform them to Claude Code format
|
// Process messages and transform them to Claude Code format
|
||||||
var anthropicMessages []interface{}
|
|
||||||
var toolCallIDs []string // Track tool call IDs for matching with tool results
|
|
||||||
|
|
||||||
if messages := root.Get("messages"); messages.Exists() && messages.IsArray() {
|
if messages := root.Get("messages"); messages.Exists() && messages.IsArray() {
|
||||||
messages.ForEach(func(_, message gjson.Result) bool {
|
messages.ForEach(func(_, message gjson.Result) bool {
|
||||||
role := message.Get("role").String()
|
role := message.Get("role").String()
|
||||||
@@ -152,33 +148,23 @@ func ConvertOpenAIRequestToClaude(modelName string, inputRawJSON []byte, stream
|
|||||||
role = "user"
|
role = "user"
|
||||||
}
|
}
|
||||||
|
|
||||||
msg := map[string]interface{}{
|
msg := `{"role":"","content":[]}`
|
||||||
"role": role,
|
msg, _ = sjson.Set(msg, "role", role)
|
||||||
"content": []interface{}{},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle content based on its type (string or array)
|
// Handle content based on its type (string or array)
|
||||||
if contentResult.Exists() && contentResult.Type == gjson.String && contentResult.String() != "" {
|
if contentResult.Exists() && contentResult.Type == gjson.String && contentResult.String() != "" {
|
||||||
// Simple text content conversion
|
part := `{"type":"text","text":""}`
|
||||||
msg["content"] = []interface{}{
|
part, _ = sjson.Set(part, "text", contentResult.String())
|
||||||
map[string]interface{}{
|
msg, _ = sjson.SetRaw(msg, "content.-1", part)
|
||||||
"type": "text",
|
|
||||||
"text": contentResult.String(),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
} else if contentResult.Exists() && contentResult.IsArray() {
|
} else if contentResult.Exists() && contentResult.IsArray() {
|
||||||
// Array of content parts processing
|
|
||||||
var contentParts []interface{}
|
|
||||||
contentResult.ForEach(func(_, part gjson.Result) bool {
|
contentResult.ForEach(func(_, part gjson.Result) bool {
|
||||||
partType := part.Get("type").String()
|
partType := part.Get("type").String()
|
||||||
|
|
||||||
switch partType {
|
switch partType {
|
||||||
case "text":
|
case "text":
|
||||||
// Text part conversion
|
textPart := `{"type":"text","text":""}`
|
||||||
contentParts = append(contentParts, map[string]interface{}{
|
textPart, _ = sjson.Set(textPart, "text", part.Get("text").String())
|
||||||
"type": "text",
|
msg, _ = sjson.SetRaw(msg, "content.-1", textPart)
|
||||||
"text": part.Get("text").String(),
|
|
||||||
})
|
|
||||||
|
|
||||||
case "image_url":
|
case "image_url":
|
||||||
// Convert OpenAI image format to Claude Code format
|
// Convert OpenAI image format to Claude Code format
|
||||||
@@ -191,132 +177,95 @@ func ConvertOpenAIRequestToClaude(modelName string, inputRawJSON []byte, stream
|
|||||||
mediaType := strings.TrimPrefix(mediaTypePart, "data:")
|
mediaType := strings.TrimPrefix(mediaTypePart, "data:")
|
||||||
data := parts[1]
|
data := parts[1]
|
||||||
|
|
||||||
contentParts = append(contentParts, map[string]interface{}{
|
imagePart := `{"type":"image","source":{"type":"base64","media_type":"","data":""}}`
|
||||||
"type": "image",
|
imagePart, _ = sjson.Set(imagePart, "source.media_type", mediaType)
|
||||||
"source": map[string]interface{}{
|
imagePart, _ = sjson.Set(imagePart, "source.data", data)
|
||||||
"type": "base64",
|
msg, _ = sjson.SetRaw(msg, "content.-1", imagePart)
|
||||||
"media_type": mediaType,
|
|
||||||
"data": data,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
if len(contentParts) > 0 {
|
|
||||||
msg["content"] = contentParts
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Initialize empty content array for tool calls
|
|
||||||
msg["content"] = []interface{}{}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle tool calls (for assistant messages)
|
// Handle tool calls (for assistant messages)
|
||||||
if toolCalls := message.Get("tool_calls"); toolCalls.Exists() && toolCalls.IsArray() && role == "assistant" {
|
if toolCalls := message.Get("tool_calls"); toolCalls.Exists() && toolCalls.IsArray() && role == "assistant" {
|
||||||
var contentParts []interface{}
|
|
||||||
|
|
||||||
// Add existing text content if any
|
|
||||||
if existingContent, ok := msg["content"].([]interface{}); ok {
|
|
||||||
contentParts = existingContent
|
|
||||||
}
|
|
||||||
|
|
||||||
toolCalls.ForEach(func(_, toolCall gjson.Result) bool {
|
toolCalls.ForEach(func(_, toolCall gjson.Result) bool {
|
||||||
if toolCall.Get("type").String() == "function" {
|
if toolCall.Get("type").String() == "function" {
|
||||||
toolCallID := toolCall.Get("id").String()
|
toolCallID := toolCall.Get("id").String()
|
||||||
if toolCallID == "" {
|
if toolCallID == "" {
|
||||||
toolCallID = genToolCallID()
|
toolCallID = genToolCallID()
|
||||||
}
|
}
|
||||||
toolCallIDs = append(toolCallIDs, toolCallID)
|
|
||||||
|
|
||||||
function := toolCall.Get("function")
|
function := toolCall.Get("function")
|
||||||
toolUse := map[string]interface{}{
|
toolUse := `{"type":"tool_use","id":"","name":"","input":{}}`
|
||||||
"type": "tool_use",
|
toolUse, _ = sjson.Set(toolUse, "id", toolCallID)
|
||||||
"id": toolCallID,
|
toolUse, _ = sjson.Set(toolUse, "name", function.Get("name").String())
|
||||||
"name": function.Get("name").String(),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse arguments for the tool call
|
// Parse arguments for the tool call
|
||||||
if args := function.Get("arguments"); args.Exists() {
|
if args := function.Get("arguments"); args.Exists() {
|
||||||
argsStr := args.String()
|
argsStr := args.String()
|
||||||
if argsStr != "" {
|
if argsStr != "" && gjson.Valid(argsStr) {
|
||||||
var argsMap map[string]interface{}
|
argsJSON := gjson.Parse(argsStr)
|
||||||
if err := json.Unmarshal([]byte(argsStr), &argsMap); err == nil {
|
if argsJSON.IsObject() {
|
||||||
toolUse["input"] = argsMap
|
toolUse, _ = sjson.SetRaw(toolUse, "input", argsJSON.Raw)
|
||||||
} else {
|
} else {
|
||||||
toolUse["input"] = map[string]interface{}{}
|
toolUse, _ = sjson.SetRaw(toolUse, "input", "{}")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
toolUse["input"] = map[string]interface{}{}
|
toolUse, _ = sjson.SetRaw(toolUse, "input", "{}")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
toolUse["input"] = map[string]interface{}{}
|
toolUse, _ = sjson.SetRaw(toolUse, "input", "{}")
|
||||||
}
|
}
|
||||||
|
|
||||||
contentParts = append(contentParts, toolUse)
|
msg, _ = sjson.SetRaw(msg, "content.-1", toolUse)
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
msg["content"] = contentParts
|
|
||||||
}
|
}
|
||||||
|
|
||||||
anthropicMessages = append(anthropicMessages, msg)
|
out, _ = sjson.SetRaw(out, "messages.-1", msg)
|
||||||
|
|
||||||
case "tool":
|
case "tool":
|
||||||
// Handle tool result messages conversion
|
// Handle tool result messages conversion
|
||||||
toolCallID := message.Get("tool_call_id").String()
|
toolCallID := message.Get("tool_call_id").String()
|
||||||
content := message.Get("content").String()
|
content := message.Get("content").String()
|
||||||
|
|
||||||
// Create tool result message in Claude Code format
|
msg := `{"role":"user","content":[{"type":"tool_result","tool_use_id":"","content":""}]}`
|
||||||
msg := map[string]interface{}{
|
msg, _ = sjson.Set(msg, "content.0.tool_use_id", toolCallID)
|
||||||
"role": "user",
|
msg, _ = sjson.Set(msg, "content.0.content", content)
|
||||||
"content": []interface{}{
|
out, _ = sjson.SetRaw(out, "messages.-1", msg)
|
||||||
map[string]interface{}{
|
|
||||||
"type": "tool_result",
|
|
||||||
"tool_use_id": toolCallID,
|
|
||||||
"content": content,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
anthropicMessages = append(anthropicMessages, msg)
|
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set messages in the output template
|
|
||||||
if len(anthropicMessages) > 0 {
|
|
||||||
messagesJSON, _ := json.Marshal(anthropicMessages)
|
|
||||||
out, _ = sjson.SetRaw(out, "messages", string(messagesJSON))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tools mapping: OpenAI tools -> Claude Code tools
|
// Tools mapping: OpenAI tools -> Claude Code tools
|
||||||
if tools := root.Get("tools"); tools.Exists() && tools.IsArray() && len(tools.Array()) > 0 {
|
if tools := root.Get("tools"); tools.Exists() && tools.IsArray() && len(tools.Array()) > 0 {
|
||||||
var anthropicTools []interface{}
|
hasAnthropicTools := false
|
||||||
tools.ForEach(func(_, tool gjson.Result) bool {
|
tools.ForEach(func(_, tool gjson.Result) bool {
|
||||||
if tool.Get("type").String() == "function" {
|
if tool.Get("type").String() == "function" {
|
||||||
function := tool.Get("function")
|
function := tool.Get("function")
|
||||||
anthropicTool := map[string]interface{}{
|
anthropicTool := `{"name":"","description":""}`
|
||||||
"name": function.Get("name").String(),
|
anthropicTool, _ = sjson.Set(anthropicTool, "name", function.Get("name").String())
|
||||||
"description": function.Get("description").String(),
|
anthropicTool, _ = sjson.Set(anthropicTool, "description", function.Get("description").String())
|
||||||
}
|
|
||||||
|
|
||||||
// Convert parameters schema for the tool
|
// Convert parameters schema for the tool
|
||||||
if parameters := function.Get("parameters"); parameters.Exists() {
|
if parameters := function.Get("parameters"); parameters.Exists() {
|
||||||
anthropicTool["input_schema"] = parameters.Value()
|
anthropicTool, _ = sjson.SetRaw(anthropicTool, "input_schema", parameters.Raw)
|
||||||
} else if parameters = function.Get("parametersJsonSchema"); parameters.Exists() {
|
} else if parameters := function.Get("parametersJsonSchema"); parameters.Exists() {
|
||||||
anthropicTool["input_schema"] = parameters.Value()
|
anthropicTool, _ = sjson.SetRaw(anthropicTool, "input_schema", parameters.Raw)
|
||||||
}
|
}
|
||||||
|
|
||||||
anthropicTools = append(anthropicTools, anthropicTool)
|
out, _ = sjson.SetRaw(out, "tools.-1", anthropicTool)
|
||||||
|
hasAnthropicTools = true
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
|
|
||||||
if len(anthropicTools) > 0 {
|
if !hasAnthropicTools {
|
||||||
toolsJSON, _ := json.Marshal(anthropicTools)
|
out, _ = sjson.Delete(out, "tools")
|
||||||
out, _ = sjson.SetRaw(out, "tools", string(toolsJSON))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -329,18 +278,17 @@ func ConvertOpenAIRequestToClaude(modelName string, inputRawJSON []byte, stream
|
|||||||
case "none":
|
case "none":
|
||||||
// Don't set tool_choice, Claude Code will not use tools
|
// Don't set tool_choice, Claude Code will not use tools
|
||||||
case "auto":
|
case "auto":
|
||||||
out, _ = sjson.Set(out, "tool_choice", map[string]interface{}{"type": "auto"})
|
out, _ = sjson.SetRaw(out, "tool_choice", `{"type":"auto"}`)
|
||||||
case "required":
|
case "required":
|
||||||
out, _ = sjson.Set(out, "tool_choice", map[string]interface{}{"type": "any"})
|
out, _ = sjson.SetRaw(out, "tool_choice", `{"type":"any"}`)
|
||||||
}
|
}
|
||||||
case gjson.JSON:
|
case gjson.JSON:
|
||||||
// Specific tool choice mapping
|
// Specific tool choice mapping
|
||||||
if toolChoice.Get("type").String() == "function" {
|
if toolChoice.Get("type").String() == "function" {
|
||||||
functionName := toolChoice.Get("function.name").String()
|
functionName := toolChoice.Get("function.name").String()
|
||||||
out, _ = sjson.Set(out, "tool_choice", map[string]interface{}{
|
toolChoiceJSON := `{"type":"tool","name":""}`
|
||||||
"type": "tool",
|
toolChoiceJSON, _ = sjson.Set(toolChoiceJSON, "name", functionName)
|
||||||
"name": functionName,
|
out, _ = sjson.SetRaw(out, "tool_choice", toolChoiceJSON)
|
||||||
})
|
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ package chat_completions
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -178,18 +178,11 @@ func ConvertClaudeResponseToOpenAI(_ context.Context, modelName string, original
|
|||||||
if arguments == "" {
|
if arguments == "" {
|
||||||
arguments = "{}"
|
arguments = "{}"
|
||||||
}
|
}
|
||||||
|
template, _ = sjson.Set(template, "choices.0.delta.tool_calls.0.index", index)
|
||||||
toolCall := map[string]interface{}{
|
template, _ = sjson.Set(template, "choices.0.delta.tool_calls.0.id", accumulator.ID)
|
||||||
"index": index,
|
template, _ = sjson.Set(template, "choices.0.delta.tool_calls.0.type", "function")
|
||||||
"id": accumulator.ID,
|
template, _ = sjson.Set(template, "choices.0.delta.tool_calls.0.function.name", accumulator.Name)
|
||||||
"type": "function",
|
template, _ = sjson.Set(template, "choices.0.delta.tool_calls.0.function.arguments", arguments)
|
||||||
"function": map[string]interface{}{
|
|
||||||
"name": accumulator.Name,
|
|
||||||
"arguments": arguments,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
template, _ = sjson.Set(template, "choices.0.delta.tool_calls", []interface{}{toolCall})
|
|
||||||
|
|
||||||
// Clean up the accumulator for this index
|
// Clean up the accumulator for this index
|
||||||
delete((*param).(*ConvertAnthropicResponseToOpenAIParams).ToolCallsAccumulator, index)
|
delete((*param).(*ConvertAnthropicResponseToOpenAIParams).ToolCallsAccumulator, index)
|
||||||
@@ -210,12 +203,11 @@ func ConvertClaudeResponseToOpenAI(_ context.Context, modelName string, original
|
|||||||
|
|
||||||
// Handle usage information for token counts
|
// Handle usage information for token counts
|
||||||
if usage := root.Get("usage"); usage.Exists() {
|
if usage := root.Get("usage"); usage.Exists() {
|
||||||
usageObj := map[string]interface{}{
|
inputTokens := usage.Get("input_tokens").Int()
|
||||||
"prompt_tokens": usage.Get("input_tokens").Int(),
|
outputTokens := usage.Get("output_tokens").Int()
|
||||||
"completion_tokens": usage.Get("output_tokens").Int(),
|
template, _ = sjson.Set(template, "usage.prompt_tokens", inputTokens)
|
||||||
"total_tokens": usage.Get("input_tokens").Int() + usage.Get("output_tokens").Int(),
|
template, _ = sjson.Set(template, "usage.completion_tokens", outputTokens)
|
||||||
}
|
template, _ = sjson.Set(template, "usage.total_tokens", inputTokens+outputTokens)
|
||||||
template, _ = sjson.Set(template, "usage", usageObj)
|
|
||||||
}
|
}
|
||||||
return []string{template}
|
return []string{template}
|
||||||
|
|
||||||
@@ -230,14 +222,10 @@ func ConvertClaudeResponseToOpenAI(_ context.Context, modelName string, original
|
|||||||
case "error":
|
case "error":
|
||||||
// Error event - format and return error response
|
// Error event - format and return error response
|
||||||
if errorData := root.Get("error"); errorData.Exists() {
|
if errorData := root.Get("error"); errorData.Exists() {
|
||||||
errorResponse := map[string]interface{}{
|
errorJSON := `{"error":{"message":"","type":""}}`
|
||||||
"error": map[string]interface{}{
|
errorJSON, _ = sjson.Set(errorJSON, "error.message", errorData.Get("message").String())
|
||||||
"message": errorData.Get("message").String(),
|
errorJSON, _ = sjson.Set(errorJSON, "error.type", errorData.Get("type").String())
|
||||||
"type": errorData.Get("type").String(),
|
return []string{errorJSON}
|
||||||
},
|
|
||||||
}
|
|
||||||
errorJSON, _ := json.Marshal(errorResponse)
|
|
||||||
return []string{string(errorJSON)}
|
|
||||||
}
|
}
|
||||||
return []string{}
|
return []string{}
|
||||||
|
|
||||||
@@ -298,10 +286,7 @@ func ConvertClaudeResponseToOpenAINonStream(_ context.Context, _ string, origina
|
|||||||
var stopReason string
|
var stopReason string
|
||||||
var contentParts []string
|
var contentParts []string
|
||||||
var reasoningParts []string
|
var reasoningParts []string
|
||||||
// Use map to track tool calls by index for proper merging
|
toolCallsAccumulator := make(map[int]*ToolCallAccumulator)
|
||||||
toolCallsMap := make(map[int]map[string]interface{})
|
|
||||||
// Track tool call arguments accumulation
|
|
||||||
toolCallArgsMap := make(map[int]strings.Builder)
|
|
||||||
|
|
||||||
for _, chunk := range chunks {
|
for _, chunk := range chunks {
|
||||||
root := gjson.ParseBytes(chunk)
|
root := gjson.ParseBytes(chunk)
|
||||||
@@ -327,18 +312,12 @@ func ConvertClaudeResponseToOpenAINonStream(_ context.Context, _ string, origina
|
|||||||
// Start of thinking/reasoning content - skip for now as it's handled in delta
|
// Start of thinking/reasoning content - skip for now as it's handled in delta
|
||||||
continue
|
continue
|
||||||
} else if blockType == "tool_use" {
|
} else if blockType == "tool_use" {
|
||||||
// Initialize tool call tracking for this index
|
// Initialize tool call accumulator for this index
|
||||||
index := int(root.Get("index").Int())
|
index := int(root.Get("index").Int())
|
||||||
toolCallsMap[index] = map[string]interface{}{
|
toolCallsAccumulator[index] = &ToolCallAccumulator{
|
||||||
"id": contentBlock.Get("id").String(),
|
ID: contentBlock.Get("id").String(),
|
||||||
"type": "function",
|
Name: contentBlock.Get("name").String(),
|
||||||
"function": map[string]interface{}{
|
|
||||||
"name": contentBlock.Get("name").String(),
|
|
||||||
"arguments": "",
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
// Initialize arguments builder for this tool call
|
|
||||||
toolCallArgsMap[index] = strings.Builder{}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -361,9 +340,8 @@ func ConvertClaudeResponseToOpenAINonStream(_ context.Context, _ string, origina
|
|||||||
// Accumulate tool call arguments
|
// Accumulate tool call arguments
|
||||||
if partialJSON := delta.Get("partial_json"); partialJSON.Exists() {
|
if partialJSON := delta.Get("partial_json"); partialJSON.Exists() {
|
||||||
index := int(root.Get("index").Int())
|
index := int(root.Get("index").Int())
|
||||||
if builder, exists := toolCallArgsMap[index]; exists {
|
if accumulator, exists := toolCallsAccumulator[index]; exists {
|
||||||
builder.WriteString(partialJSON.String())
|
accumulator.Arguments.WriteString(partialJSON.String())
|
||||||
toolCallArgsMap[index] = builder
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -372,14 +350,9 @@ func ConvertClaudeResponseToOpenAINonStream(_ context.Context, _ string, origina
|
|||||||
case "content_block_stop":
|
case "content_block_stop":
|
||||||
// Finalize tool call arguments for this index when content block ends
|
// Finalize tool call arguments for this index when content block ends
|
||||||
index := int(root.Get("index").Int())
|
index := int(root.Get("index").Int())
|
||||||
if toolCall, exists := toolCallsMap[index]; exists {
|
if accumulator, exists := toolCallsAccumulator[index]; exists {
|
||||||
if builder, argsExists := toolCallArgsMap[index]; argsExists {
|
if accumulator.Arguments.Len() == 0 {
|
||||||
// Set the accumulated arguments for the tool call
|
accumulator.Arguments.WriteString("{}")
|
||||||
arguments := builder.String()
|
|
||||||
if arguments == "" {
|
|
||||||
arguments = "{}"
|
|
||||||
}
|
|
||||||
toolCall["function"].(map[string]interface{})["arguments"] = arguments
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -417,24 +390,35 @@ func ConvertClaudeResponseToOpenAINonStream(_ context.Context, _ string, origina
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Set tool calls if any were accumulated during processing
|
// Set tool calls if any were accumulated during processing
|
||||||
if len(toolCallsMap) > 0 {
|
if len(toolCallsAccumulator) > 0 {
|
||||||
// Convert tool calls map to array, preserving order by index
|
toolCallsCount := 0
|
||||||
var toolCallsArray []interface{}
|
|
||||||
// Find the maximum index to determine the range
|
|
||||||
maxIndex := -1
|
maxIndex := -1
|
||||||
for index := range toolCallsMap {
|
for index := range toolCallsAccumulator {
|
||||||
if index > maxIndex {
|
if index > maxIndex {
|
||||||
maxIndex = index
|
maxIndex = index
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Iterate through all possible indices up to maxIndex
|
|
||||||
for i := 0; i <= maxIndex; i++ {
|
for i := 0; i <= maxIndex; i++ {
|
||||||
if toolCall, exists := toolCallsMap[i]; exists {
|
accumulator, exists := toolCallsAccumulator[i]
|
||||||
toolCallsArray = append(toolCallsArray, toolCall)
|
if !exists {
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
arguments := accumulator.Arguments.String()
|
||||||
|
|
||||||
|
idPath := fmt.Sprintf("choices.0.message.tool_calls.%d.id", toolCallsCount)
|
||||||
|
typePath := fmt.Sprintf("choices.0.message.tool_calls.%d.type", toolCallsCount)
|
||||||
|
namePath := fmt.Sprintf("choices.0.message.tool_calls.%d.function.name", toolCallsCount)
|
||||||
|
argumentsPath := fmt.Sprintf("choices.0.message.tool_calls.%d.function.arguments", toolCallsCount)
|
||||||
|
|
||||||
|
out, _ = sjson.Set(out, idPath, accumulator.ID)
|
||||||
|
out, _ = sjson.Set(out, typePath, "function")
|
||||||
|
out, _ = sjson.Set(out, namePath, accumulator.Name)
|
||||||
|
out, _ = sjson.Set(out, argumentsPath, arguments)
|
||||||
|
toolCallsCount++
|
||||||
}
|
}
|
||||||
if len(toolCallsArray) > 0 {
|
if toolCallsCount > 0 {
|
||||||
out, _ = sjson.Set(out, "choices.0.message.tool_calls", toolCallsArray)
|
|
||||||
out, _ = sjson.Set(out, "choices.0.finish_reason", "tool_calls")
|
out, _ = sjson.Set(out, "choices.0.finish_reason", "tool_calls")
|
||||||
} else {
|
} else {
|
||||||
out, _ = sjson.Set(out, "choices.0.finish_reason", mapAnthropicStopReasonToOpenAI(stopReason))
|
out, _ = sjson.Set(out, "choices.0.finish_reason", mapAnthropicStopReasonToOpenAI(stopReason))
|
||||||
|
|||||||
@@ -254,7 +254,10 @@ func ConvertOpenAIResponsesRequestToClaude(modelName string, inputRawJSON []byte
|
|||||||
toolUse, _ = sjson.Set(toolUse, "id", callID)
|
toolUse, _ = sjson.Set(toolUse, "id", callID)
|
||||||
toolUse, _ = sjson.Set(toolUse, "name", name)
|
toolUse, _ = sjson.Set(toolUse, "name", name)
|
||||||
if argsStr != "" && gjson.Valid(argsStr) {
|
if argsStr != "" && gjson.Valid(argsStr) {
|
||||||
toolUse, _ = sjson.SetRaw(toolUse, "input", argsStr)
|
argsJSON := gjson.Parse(argsStr)
|
||||||
|
if argsJSON.IsObject() {
|
||||||
|
toolUse, _ = sjson.SetRaw(toolUse, "input", argsJSON.Raw)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
asst := `{"role":"assistant","content":[]}`
|
asst := `{"role":"assistant","content":[]}`
|
||||||
@@ -309,16 +312,18 @@ func ConvertOpenAIResponsesRequestToClaude(modelName string, inputRawJSON []byte
|
|||||||
case gjson.String:
|
case gjson.String:
|
||||||
switch toolChoice.String() {
|
switch toolChoice.String() {
|
||||||
case "auto":
|
case "auto":
|
||||||
out, _ = sjson.Set(out, "tool_choice", map[string]interface{}{"type": "auto"})
|
out, _ = sjson.SetRaw(out, "tool_choice", `{"type":"auto"}`)
|
||||||
case "none":
|
case "none":
|
||||||
// Leave unset; implies no tools
|
// Leave unset; implies no tools
|
||||||
case "required":
|
case "required":
|
||||||
out, _ = sjson.Set(out, "tool_choice", map[string]interface{}{"type": "any"})
|
out, _ = sjson.SetRaw(out, "tool_choice", `{"type":"any"}`)
|
||||||
}
|
}
|
||||||
case gjson.JSON:
|
case gjson.JSON:
|
||||||
if toolChoice.Get("type").String() == "function" {
|
if toolChoice.Get("type").String() == "function" {
|
||||||
fn := toolChoice.Get("function.name").String()
|
fn := toolChoice.Get("function.name").String()
|
||||||
out, _ = sjson.Set(out, "tool_choice", map[string]interface{}{"type": "tool", "name": fn})
|
toolChoiceJSON := `{"name":"","type":"tool"}`
|
||||||
|
toolChoiceJSON, _ = sjson.Set(toolChoiceJSON, "name", fn)
|
||||||
|
out, _ = sjson.SetRaw(out, "tool_choice", toolChoiceJSON)
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
|
|
||||||
|
|||||||
@@ -344,31 +344,20 @@ func ConvertClaudeResponseToOpenAIResponses(ctx context.Context, modelName strin
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Build response.output from aggregated state
|
// Build response.output from aggregated state
|
||||||
var outputs []interface{}
|
outputsWrapper := `{"arr":[]}`
|
||||||
// reasoning item (if any)
|
// reasoning item (if any)
|
||||||
if st.ReasoningBuf.Len() > 0 || st.ReasoningPartAdded {
|
if st.ReasoningBuf.Len() > 0 || st.ReasoningPartAdded {
|
||||||
r := map[string]interface{}{
|
item := `{"id":"","type":"reasoning","summary":[{"type":"summary_text","text":""}]}`
|
||||||
"id": st.ReasoningItemID,
|
item, _ = sjson.Set(item, "id", st.ReasoningItemID)
|
||||||
"type": "reasoning",
|
item, _ = sjson.Set(item, "summary.0.text", st.ReasoningBuf.String())
|
||||||
"summary": []interface{}{map[string]interface{}{"type": "summary_text", "text": st.ReasoningBuf.String()}},
|
outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", item)
|
||||||
}
|
|
||||||
outputs = append(outputs, r)
|
|
||||||
}
|
}
|
||||||
// assistant message item (if any text)
|
// assistant message item (if any text)
|
||||||
if st.TextBuf.Len() > 0 || st.InTextBlock || st.CurrentMsgID != "" {
|
if st.TextBuf.Len() > 0 || st.InTextBlock || st.CurrentMsgID != "" {
|
||||||
m := map[string]interface{}{
|
item := `{"id":"","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":""}],"role":"assistant"}`
|
||||||
"id": st.CurrentMsgID,
|
item, _ = sjson.Set(item, "id", st.CurrentMsgID)
|
||||||
"type": "message",
|
item, _ = sjson.Set(item, "content.0.text", st.TextBuf.String())
|
||||||
"status": "completed",
|
outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", item)
|
||||||
"content": []interface{}{map[string]interface{}{
|
|
||||||
"type": "output_text",
|
|
||||||
"annotations": []interface{}{},
|
|
||||||
"logprobs": []interface{}{},
|
|
||||||
"text": st.TextBuf.String(),
|
|
||||||
}},
|
|
||||||
"role": "assistant",
|
|
||||||
}
|
|
||||||
outputs = append(outputs, m)
|
|
||||||
}
|
}
|
||||||
// function_call items (in ascending index order for determinism)
|
// function_call items (in ascending index order for determinism)
|
||||||
if len(st.FuncArgsBuf) > 0 {
|
if len(st.FuncArgsBuf) > 0 {
|
||||||
@@ -395,19 +384,16 @@ func ConvertClaudeResponseToOpenAIResponses(ctx context.Context, modelName strin
|
|||||||
if callID == "" && st.CurrentFCID != "" {
|
if callID == "" && st.CurrentFCID != "" {
|
||||||
callID = st.CurrentFCID
|
callID = st.CurrentFCID
|
||||||
}
|
}
|
||||||
item := map[string]interface{}{
|
item := `{"id":"","type":"function_call","status":"completed","arguments":"","call_id":"","name":""}`
|
||||||
"id": fmt.Sprintf("fc_%s", callID),
|
item, _ = sjson.Set(item, "id", fmt.Sprintf("fc_%s", callID))
|
||||||
"type": "function_call",
|
item, _ = sjson.Set(item, "arguments", args)
|
||||||
"status": "completed",
|
item, _ = sjson.Set(item, "call_id", callID)
|
||||||
"arguments": args,
|
item, _ = sjson.Set(item, "name", name)
|
||||||
"call_id": callID,
|
outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", item)
|
||||||
"name": name,
|
|
||||||
}
|
|
||||||
outputs = append(outputs, item)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(outputs) > 0 {
|
if gjson.Get(outputsWrapper, "arr.#").Int() > 0 {
|
||||||
completed, _ = sjson.Set(completed, "response.output", outputs)
|
completed, _ = sjson.SetRaw(completed, "response.output", gjson.Get(outputsWrapper, "arr").Raw)
|
||||||
}
|
}
|
||||||
|
|
||||||
reasoningTokens := int64(0)
|
reasoningTokens := int64(0)
|
||||||
@@ -628,27 +614,18 @@ func ConvertClaudeResponseToOpenAIResponsesNonStream(_ context.Context, _ string
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Build output array
|
// Build output array
|
||||||
var outputs []interface{}
|
outputsWrapper := `{"arr":[]}`
|
||||||
if reasoningBuf.Len() > 0 {
|
if reasoningBuf.Len() > 0 {
|
||||||
outputs = append(outputs, map[string]interface{}{
|
item := `{"id":"","type":"reasoning","summary":[{"type":"summary_text","text":""}]}`
|
||||||
"id": reasoningItemID,
|
item, _ = sjson.Set(item, "id", reasoningItemID)
|
||||||
"type": "reasoning",
|
item, _ = sjson.Set(item, "summary.0.text", reasoningBuf.String())
|
||||||
"summary": []interface{}{map[string]interface{}{"type": "summary_text", "text": reasoningBuf.String()}},
|
outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", item)
|
||||||
})
|
|
||||||
}
|
}
|
||||||
if currentMsgID != "" || textBuf.Len() > 0 {
|
if currentMsgID != "" || textBuf.Len() > 0 {
|
||||||
outputs = append(outputs, map[string]interface{}{
|
item := `{"id":"","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":""}],"role":"assistant"}`
|
||||||
"id": currentMsgID,
|
item, _ = sjson.Set(item, "id", currentMsgID)
|
||||||
"type": "message",
|
item, _ = sjson.Set(item, "content.0.text", textBuf.String())
|
||||||
"status": "completed",
|
outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", item)
|
||||||
"content": []interface{}{map[string]interface{}{
|
|
||||||
"type": "output_text",
|
|
||||||
"annotations": []interface{}{},
|
|
||||||
"logprobs": []interface{}{},
|
|
||||||
"text": textBuf.String(),
|
|
||||||
}},
|
|
||||||
"role": "assistant",
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
if len(toolCalls) > 0 {
|
if len(toolCalls) > 0 {
|
||||||
// Preserve index order
|
// Preserve index order
|
||||||
@@ -669,18 +646,16 @@ func ConvertClaudeResponseToOpenAIResponsesNonStream(_ context.Context, _ string
|
|||||||
if args == "" {
|
if args == "" {
|
||||||
args = "{}"
|
args = "{}"
|
||||||
}
|
}
|
||||||
outputs = append(outputs, map[string]interface{}{
|
item := `{"id":"","type":"function_call","status":"completed","arguments":"","call_id":"","name":""}`
|
||||||
"id": fmt.Sprintf("fc_%s", st.id),
|
item, _ = sjson.Set(item, "id", fmt.Sprintf("fc_%s", st.id))
|
||||||
"type": "function_call",
|
item, _ = sjson.Set(item, "arguments", args)
|
||||||
"status": "completed",
|
item, _ = sjson.Set(item, "call_id", st.id)
|
||||||
"arguments": args,
|
item, _ = sjson.Set(item, "name", st.name)
|
||||||
"call_id": st.id,
|
outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", item)
|
||||||
"name": st.name,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(outputs) > 0 {
|
if gjson.Get(outputsWrapper, "arr.#").Int() > 0 {
|
||||||
out, _ = sjson.Set(out, "output", outputs)
|
out, _ = sjson.SetRaw(out, "output", gjson.Get(outputsWrapper, "arr").Raw)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Usage
|
// Usage
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ package claude
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -191,21 +190,12 @@ func ConvertCodexResponseToClaudeNonStream(_ context.Context, _ string, original
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
response := map[string]interface{}{
|
out := `{"id":"","type":"message","role":"assistant","model":"","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":0,"output_tokens":0}}`
|
||||||
"id": responseData.Get("id").String(),
|
out, _ = sjson.Set(out, "id", responseData.Get("id").String())
|
||||||
"type": "message",
|
out, _ = sjson.Set(out, "model", responseData.Get("model").String())
|
||||||
"role": "assistant",
|
out, _ = sjson.Set(out, "usage.input_tokens", responseData.Get("usage.input_tokens").Int())
|
||||||
"model": responseData.Get("model").String(),
|
out, _ = sjson.Set(out, "usage.output_tokens", responseData.Get("usage.output_tokens").Int())
|
||||||
"content": []interface{}{},
|
|
||||||
"stop_reason": nil,
|
|
||||||
"stop_sequence": nil,
|
|
||||||
"usage": map[string]interface{}{
|
|
||||||
"input_tokens": responseData.Get("usage.input_tokens").Int(),
|
|
||||||
"output_tokens": responseData.Get("usage.output_tokens").Int(),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
var contentBlocks []interface{}
|
|
||||||
hasToolCall := false
|
hasToolCall := false
|
||||||
|
|
||||||
if output := responseData.Get("output"); output.Exists() && output.IsArray() {
|
if output := responseData.Get("output"); output.Exists() && output.IsArray() {
|
||||||
@@ -244,10 +234,9 @@ func ConvertCodexResponseToClaudeNonStream(_ context.Context, _ string, original
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if thinkingBuilder.Len() > 0 {
|
if thinkingBuilder.Len() > 0 {
|
||||||
contentBlocks = append(contentBlocks, map[string]interface{}{
|
block := `{"type":"thinking","thinking":""}`
|
||||||
"type": "thinking",
|
block, _ = sjson.Set(block, "thinking", thinkingBuilder.String())
|
||||||
"thinking": thinkingBuilder.String(),
|
out, _ = sjson.SetRaw(out, "content.-1", block)
|
||||||
})
|
|
||||||
}
|
}
|
||||||
case "message":
|
case "message":
|
||||||
if content := item.Get("content"); content.Exists() {
|
if content := item.Get("content"); content.Exists() {
|
||||||
@@ -256,10 +245,9 @@ func ConvertCodexResponseToClaudeNonStream(_ context.Context, _ string, original
|
|||||||
if part.Get("type").String() == "output_text" {
|
if part.Get("type").String() == "output_text" {
|
||||||
text := part.Get("text").String()
|
text := part.Get("text").String()
|
||||||
if text != "" {
|
if text != "" {
|
||||||
contentBlocks = append(contentBlocks, map[string]interface{}{
|
block := `{"type":"text","text":""}`
|
||||||
"type": "text",
|
block, _ = sjson.Set(block, "text", text)
|
||||||
"text": text,
|
out, _ = sjson.SetRaw(out, "content.-1", block)
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
@@ -267,10 +255,9 @@ func ConvertCodexResponseToClaudeNonStream(_ context.Context, _ string, original
|
|||||||
} else {
|
} else {
|
||||||
text := content.String()
|
text := content.String()
|
||||||
if text != "" {
|
if text != "" {
|
||||||
contentBlocks = append(contentBlocks, map[string]interface{}{
|
block := `{"type":"text","text":""}`
|
||||||
"type": "text",
|
block, _ = sjson.Set(block, "text", text)
|
||||||
"text": text,
|
out, _ = sjson.SetRaw(out, "content.-1", block)
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -281,54 +268,41 @@ func ConvertCodexResponseToClaudeNonStream(_ context.Context, _ string, original
|
|||||||
name = original
|
name = original
|
||||||
}
|
}
|
||||||
|
|
||||||
toolBlock := map[string]interface{}{
|
toolBlock := `{"type":"tool_use","id":"","name":"","input":{}}`
|
||||||
"type": "tool_use",
|
toolBlock, _ = sjson.Set(toolBlock, "id", item.Get("call_id").String())
|
||||||
"id": item.Get("call_id").String(),
|
toolBlock, _ = sjson.Set(toolBlock, "name", name)
|
||||||
"name": name,
|
inputRaw := "{}"
|
||||||
"input": map[string]interface{}{},
|
if argsStr := item.Get("arguments").String(); argsStr != "" && gjson.Valid(argsStr) {
|
||||||
}
|
argsJSON := gjson.Parse(argsStr)
|
||||||
|
if argsJSON.IsObject() {
|
||||||
if argsStr := item.Get("arguments").String(); argsStr != "" {
|
inputRaw = argsJSON.Raw
|
||||||
var args interface{}
|
|
||||||
if err := json.Unmarshal([]byte(argsStr), &args); err == nil {
|
|
||||||
toolBlock["input"] = args
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
toolBlock, _ = sjson.SetRaw(toolBlock, "input", inputRaw)
|
||||||
contentBlocks = append(contentBlocks, toolBlock)
|
out, _ = sjson.SetRaw(out, "content.-1", toolBlock)
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(contentBlocks) > 0 {
|
|
||||||
response["content"] = contentBlocks
|
|
||||||
}
|
|
||||||
|
|
||||||
if stopReason := responseData.Get("stop_reason"); stopReason.Exists() && stopReason.String() != "" {
|
if stopReason := responseData.Get("stop_reason"); stopReason.Exists() && stopReason.String() != "" {
|
||||||
response["stop_reason"] = stopReason.String()
|
out, _ = sjson.Set(out, "stop_reason", stopReason.String())
|
||||||
} else if hasToolCall {
|
} else if hasToolCall {
|
||||||
response["stop_reason"] = "tool_use"
|
out, _ = sjson.Set(out, "stop_reason", "tool_use")
|
||||||
} else {
|
} else {
|
||||||
response["stop_reason"] = "end_turn"
|
out, _ = sjson.Set(out, "stop_reason", "end_turn")
|
||||||
}
|
}
|
||||||
|
|
||||||
if stopSequence := responseData.Get("stop_sequence"); stopSequence.Exists() && stopSequence.String() != "" {
|
if stopSequence := responseData.Get("stop_sequence"); stopSequence.Exists() && stopSequence.String() != "" {
|
||||||
response["stop_sequence"] = stopSequence.Value()
|
out, _ = sjson.SetRaw(out, "stop_sequence", stopSequence.Raw)
|
||||||
}
|
}
|
||||||
|
|
||||||
if responseData.Get("usage.input_tokens").Exists() || responseData.Get("usage.output_tokens").Exists() {
|
if responseData.Get("usage.input_tokens").Exists() || responseData.Get("usage.output_tokens").Exists() {
|
||||||
response["usage"] = map[string]interface{}{
|
out, _ = sjson.Set(out, "usage.input_tokens", responseData.Get("usage.input_tokens").Int())
|
||||||
"input_tokens": responseData.Get("usage.input_tokens").Int(),
|
out, _ = sjson.Set(out, "usage.output_tokens", responseData.Get("usage.output_tokens").Int())
|
||||||
"output_tokens": responseData.Get("usage.output_tokens").Int(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
responseJSON, err := json.Marshal(response)
|
return out
|
||||||
if err != nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return string(responseJSON)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// buildReverseMapFromClaudeOriginalShortToOriginal builds a map[short]original from original Claude request tools.
|
// buildReverseMapFromClaudeOriginalShortToOriginal builds a map[short]original from original Claude request tools.
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ package gemini
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -190,20 +189,20 @@ func ConvertCodexResponseToGeminiNonStream(_ context.Context, modelName string,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Process output content to build parts array
|
// Process output content to build parts array
|
||||||
var parts []interface{}
|
|
||||||
hasToolCall := false
|
hasToolCall := false
|
||||||
var pendingFunctionCalls []interface{}
|
var pendingFunctionCalls []string
|
||||||
|
|
||||||
flushPendingFunctionCalls := func() {
|
flushPendingFunctionCalls := func() {
|
||||||
if len(pendingFunctionCalls) > 0 {
|
if len(pendingFunctionCalls) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
// Add all pending function calls as individual parts
|
// Add all pending function calls as individual parts
|
||||||
// This maintains the original Gemini API format while ensuring consecutive calls are grouped together
|
// This maintains the original Gemini API format while ensuring consecutive calls are grouped together
|
||||||
for _, fc := range pendingFunctionCalls {
|
for _, fc := range pendingFunctionCalls {
|
||||||
parts = append(parts, fc)
|
template, _ = sjson.SetRaw(template, "candidates.0.content.parts.-1", fc)
|
||||||
}
|
}
|
||||||
pendingFunctionCalls = nil
|
pendingFunctionCalls = nil
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if output := responseData.Get("output"); output.Exists() && output.IsArray() {
|
if output := responseData.Get("output"); output.Exists() && output.IsArray() {
|
||||||
output.ForEach(func(key, value gjson.Result) bool {
|
output.ForEach(func(key, value gjson.Result) bool {
|
||||||
@@ -216,11 +215,9 @@ func ConvertCodexResponseToGeminiNonStream(_ context.Context, modelName string,
|
|||||||
|
|
||||||
// Add thinking content
|
// Add thinking content
|
||||||
if content := value.Get("content"); content.Exists() {
|
if content := value.Get("content"); content.Exists() {
|
||||||
part := map[string]interface{}{
|
part := `{"text":"","thought":true}`
|
||||||
"thought": true,
|
part, _ = sjson.Set(part, "text", content.String())
|
||||||
"text": content.String(),
|
template, _ = sjson.SetRaw(template, "candidates.0.content.parts.-1", part)
|
||||||
}
|
|
||||||
parts = append(parts, part)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case "message":
|
case "message":
|
||||||
@@ -232,10 +229,9 @@ func ConvertCodexResponseToGeminiNonStream(_ context.Context, modelName string,
|
|||||||
content.ForEach(func(_, contentItem gjson.Result) bool {
|
content.ForEach(func(_, contentItem gjson.Result) bool {
|
||||||
if contentItem.Get("type").String() == "output_text" {
|
if contentItem.Get("type").String() == "output_text" {
|
||||||
if text := contentItem.Get("text"); text.Exists() {
|
if text := contentItem.Get("text"); text.Exists() {
|
||||||
part := map[string]interface{}{
|
part := `{"text":""}`
|
||||||
"text": text.String(),
|
part, _ = sjson.Set(part, "text", text.String())
|
||||||
}
|
template, _ = sjson.SetRaw(template, "candidates.0.content.parts.-1", part)
|
||||||
parts = append(parts, part)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
@@ -245,28 +241,21 @@ func ConvertCodexResponseToGeminiNonStream(_ context.Context, modelName string,
|
|||||||
case "function_call":
|
case "function_call":
|
||||||
// Collect function call for potential merging with consecutive ones
|
// Collect function call for potential merging with consecutive ones
|
||||||
hasToolCall = true
|
hasToolCall = true
|
||||||
functionCall := map[string]interface{}{
|
functionCall := `{"functionCall":{"args":{},"name":""}}`
|
||||||
"functionCall": map[string]interface{}{
|
{
|
||||||
"name": func() string {
|
|
||||||
n := value.Get("name").String()
|
n := value.Get("name").String()
|
||||||
rev := buildReverseMapFromGeminiOriginal(originalRequestRawJSON)
|
rev := buildReverseMapFromGeminiOriginal(originalRequestRawJSON)
|
||||||
if orig, ok := rev[n]; ok {
|
if orig, ok := rev[n]; ok {
|
||||||
return orig
|
n = orig
|
||||||
}
|
}
|
||||||
return n
|
functionCall, _ = sjson.Set(functionCall, "functionCall.name", n)
|
||||||
}(),
|
|
||||||
"args": map[string]interface{}{},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse and set arguments
|
// Parse and set arguments
|
||||||
if argsStr := value.Get("arguments").String(); argsStr != "" {
|
if argsStr := value.Get("arguments").String(); argsStr != "" {
|
||||||
argsResult := gjson.Parse(argsStr)
|
argsResult := gjson.Parse(argsStr)
|
||||||
if argsResult.IsObject() {
|
if argsResult.IsObject() {
|
||||||
var args map[string]interface{}
|
functionCall, _ = sjson.SetRaw(functionCall, "functionCall.args", argsStr)
|
||||||
if err := json.Unmarshal([]byte(argsStr), &args); err == nil {
|
|
||||||
functionCall["functionCall"].(map[string]interface{})["args"] = args
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -279,11 +268,6 @@ func ConvertCodexResponseToGeminiNonStream(_ context.Context, modelName string,
|
|||||||
flushPendingFunctionCalls()
|
flushPendingFunctionCalls()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the parts array
|
|
||||||
if len(parts) > 0 {
|
|
||||||
template, _ = sjson.SetRaw(template, "candidates.0.content.parts", mustMarshalJSON(parts))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set finish reason based on whether there were tool calls
|
// Set finish reason based on whether there were tool calls
|
||||||
if hasToolCall {
|
if hasToolCall {
|
||||||
template, _ = sjson.Set(template, "candidates.0.finishReason", "STOP")
|
template, _ = sjson.Set(template, "candidates.0.finishReason", "STOP")
|
||||||
@@ -323,15 +307,6 @@ func buildReverseMapFromGeminiOriginal(original []byte) map[string]string {
|
|||||||
return rev
|
return rev
|
||||||
}
|
}
|
||||||
|
|
||||||
// mustMarshalJSON marshals a value to JSON, panicking on error.
|
|
||||||
func mustMarshalJSON(v interface{}) string {
|
|
||||||
data, err := json.Marshal(v)
|
|
||||||
if err != nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return string(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
func GeminiTokenCount(ctx context.Context, count int64) string {
|
func GeminiTokenCount(ctx context.Context, count int64) string {
|
||||||
return fmt.Sprintf(`{"totalTokens":%d,"promptTokensDetails":[{"modality":"TEXT","tokenCount":%d}]}`, count, count)
|
return fmt.Sprintf(`{"totalTokens":%d,"promptTokensDetails":[{"modality":"TEXT","tokenCount":%d}]}`, count, count)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,10 +7,8 @@ package claude
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
client "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces"
|
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini/common"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini/common"
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
|
||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
@@ -41,92 +39,100 @@ func ConvertClaudeRequestToCLI(modelName string, inputRawJSON []byte, _ bool) []
|
|||||||
rawJSON := bytes.Clone(inputRawJSON)
|
rawJSON := bytes.Clone(inputRawJSON)
|
||||||
rawJSON = bytes.Replace(rawJSON, []byte(`"url":{"type":"string","format":"uri",`), []byte(`"url":{"type":"string",`), -1)
|
rawJSON = bytes.Replace(rawJSON, []byte(`"url":{"type":"string","format":"uri",`), []byte(`"url":{"type":"string",`), -1)
|
||||||
|
|
||||||
|
// Build output Gemini CLI request JSON
|
||||||
|
out := `{"model":"","request":{"contents":[]}}`
|
||||||
|
out, _ = sjson.Set(out, "model", modelName)
|
||||||
|
|
||||||
// system instruction
|
// system instruction
|
||||||
var systemInstruction *client.Content
|
if systemResult := gjson.GetBytes(rawJSON, "system"); systemResult.IsArray() {
|
||||||
systemResult := gjson.GetBytes(rawJSON, "system")
|
systemInstruction := `{"role":"user","parts":[]}`
|
||||||
if systemResult.IsArray() {
|
hasSystemParts := false
|
||||||
systemResults := systemResult.Array()
|
systemResult.ForEach(func(_, systemPromptResult gjson.Result) bool {
|
||||||
systemInstruction = &client.Content{Role: "user", Parts: []client.Part{}}
|
if systemPromptResult.Get("type").String() == "text" {
|
||||||
for i := 0; i < len(systemResults); i++ {
|
textResult := systemPromptResult.Get("text")
|
||||||
systemPromptResult := systemResults[i]
|
if textResult.Type == gjson.String {
|
||||||
systemTypePromptResult := systemPromptResult.Get("type")
|
part := `{"text":""}`
|
||||||
if systemTypePromptResult.Type == gjson.String && systemTypePromptResult.String() == "text" {
|
part, _ = sjson.Set(part, "text", textResult.String())
|
||||||
systemPrompt := systemPromptResult.Get("text").String()
|
systemInstruction, _ = sjson.SetRaw(systemInstruction, "parts.-1", part)
|
||||||
systemPart := client.Part{Text: systemPrompt}
|
hasSystemParts = true
|
||||||
systemInstruction.Parts = append(systemInstruction.Parts, systemPart)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(systemInstruction.Parts) == 0 {
|
return true
|
||||||
systemInstruction = nil
|
})
|
||||||
|
if hasSystemParts {
|
||||||
|
out, _ = sjson.SetRaw(out, "request.systemInstruction", systemInstruction)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// contents
|
// contents
|
||||||
contents := make([]client.Content, 0)
|
if messagesResult := gjson.GetBytes(rawJSON, "messages"); messagesResult.IsArray() {
|
||||||
messagesResult := gjson.GetBytes(rawJSON, "messages")
|
messagesResult.ForEach(func(_, messageResult gjson.Result) bool {
|
||||||
if messagesResult.IsArray() {
|
|
||||||
messageResults := messagesResult.Array()
|
|
||||||
for i := 0; i < len(messageResults); i++ {
|
|
||||||
messageResult := messageResults[i]
|
|
||||||
roleResult := messageResult.Get("role")
|
roleResult := messageResult.Get("role")
|
||||||
if roleResult.Type != gjson.String {
|
if roleResult.Type != gjson.String {
|
||||||
continue
|
return true
|
||||||
}
|
}
|
||||||
role := roleResult.String()
|
role := roleResult.String()
|
||||||
if role == "assistant" {
|
if role == "assistant" {
|
||||||
role = "model"
|
role = "model"
|
||||||
}
|
}
|
||||||
clientContent := client.Content{Role: role, Parts: []client.Part{}}
|
|
||||||
|
contentJSON := `{"role":"","parts":[]}`
|
||||||
|
contentJSON, _ = sjson.Set(contentJSON, "role", role)
|
||||||
|
|
||||||
contentsResult := messageResult.Get("content")
|
contentsResult := messageResult.Get("content")
|
||||||
if contentsResult.IsArray() {
|
if contentsResult.IsArray() {
|
||||||
contentResults := contentsResult.Array()
|
contentsResult.ForEach(func(_, contentResult gjson.Result) bool {
|
||||||
for j := 0; j < len(contentResults); j++ {
|
switch contentResult.Get("type").String() {
|
||||||
contentResult := contentResults[j]
|
case "text":
|
||||||
contentTypeResult := contentResult.Get("type")
|
part := `{"text":""}`
|
||||||
if contentTypeResult.Type == gjson.String && contentTypeResult.String() == "text" {
|
part, _ = sjson.Set(part, "text", contentResult.Get("text").String())
|
||||||
prompt := contentResult.Get("text").String()
|
contentJSON, _ = sjson.SetRaw(contentJSON, "parts.-1", part)
|
||||||
clientContent.Parts = append(clientContent.Parts, client.Part{Text: prompt})
|
|
||||||
} else if contentTypeResult.Type == gjson.String && contentTypeResult.String() == "tool_use" {
|
case "tool_use":
|
||||||
functionName := contentResult.Get("name").String()
|
functionName := contentResult.Get("name").String()
|
||||||
functionArgs := contentResult.Get("input").String()
|
functionArgs := contentResult.Get("input").String()
|
||||||
var args map[string]any
|
argsResult := gjson.Parse(functionArgs)
|
||||||
if err := json.Unmarshal([]byte(functionArgs), &args); err == nil {
|
if argsResult.IsObject() && gjson.Valid(functionArgs) {
|
||||||
clientContent.Parts = append(clientContent.Parts, client.Part{
|
part := `{"thoughtSignature":"","functionCall":{"name":"","args":{}}}`
|
||||||
FunctionCall: &client.FunctionCall{Name: functionName, Args: args},
|
part, _ = sjson.Set(part, "thoughtSignature", geminiCLIClaudeThoughtSignature)
|
||||||
ThoughtSignature: geminiCLIClaudeThoughtSignature,
|
part, _ = sjson.Set(part, "functionCall.name", functionName)
|
||||||
})
|
part, _ = sjson.SetRaw(part, "functionCall.args", functionArgs)
|
||||||
|
contentJSON, _ = sjson.SetRaw(contentJSON, "parts.-1", part)
|
||||||
}
|
}
|
||||||
} else if contentTypeResult.Type == gjson.String && contentTypeResult.String() == "tool_result" {
|
|
||||||
|
case "tool_result":
|
||||||
toolCallID := contentResult.Get("tool_use_id").String()
|
toolCallID := contentResult.Get("tool_use_id").String()
|
||||||
if toolCallID != "" {
|
if toolCallID == "" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
funcName := toolCallID
|
funcName := toolCallID
|
||||||
toolCallIDs := strings.Split(toolCallID, "-")
|
toolCallIDs := strings.Split(toolCallID, "-")
|
||||||
if len(toolCallIDs) > 1 {
|
if len(toolCallIDs) > 1 {
|
||||||
funcName = strings.Join(toolCallIDs[0:len(toolCallIDs)-1], "-")
|
funcName = strings.Join(toolCallIDs[0:len(toolCallIDs)-1], "-")
|
||||||
}
|
}
|
||||||
responseData := contentResult.Get("content").Raw
|
responseData := contentResult.Get("content").Raw
|
||||||
functionResponse := client.FunctionResponse{Name: funcName, Response: map[string]interface{}{"result": responseData}}
|
part := `{"functionResponse":{"name":"","response":{"result":""}}}`
|
||||||
clientContent.Parts = append(clientContent.Parts, client.Part{FunctionResponse: &functionResponse})
|
part, _ = sjson.Set(part, "functionResponse.name", funcName)
|
||||||
|
part, _ = sjson.Set(part, "functionResponse.response.result", responseData)
|
||||||
|
contentJSON, _ = sjson.SetRaw(contentJSON, "parts.-1", part)
|
||||||
}
|
}
|
||||||
}
|
return true
|
||||||
}
|
})
|
||||||
contents = append(contents, clientContent)
|
out, _ = sjson.SetRaw(out, "request.contents.-1", contentJSON)
|
||||||
} else if contentsResult.Type == gjson.String {
|
} else if contentsResult.Type == gjson.String {
|
||||||
prompt := contentsResult.String()
|
part := `{"text":""}`
|
||||||
contents = append(contents, client.Content{Role: role, Parts: []client.Part{{Text: prompt}}})
|
part, _ = sjson.Set(part, "text", contentsResult.String())
|
||||||
}
|
contentJSON, _ = sjson.SetRaw(contentJSON, "parts.-1", part)
|
||||||
|
out, _ = sjson.SetRaw(out, "request.contents.-1", contentJSON)
|
||||||
}
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// tools
|
// tools
|
||||||
var tools []client.ToolDeclaration
|
if toolsResult := gjson.GetBytes(rawJSON, "tools"); toolsResult.IsArray() {
|
||||||
toolsResult := gjson.GetBytes(rawJSON, "tools")
|
hasTools := false
|
||||||
if toolsResult.IsArray() {
|
toolsResult.ForEach(func(_, toolResult gjson.Result) bool {
|
||||||
tools = make([]client.ToolDeclaration, 1)
|
|
||||||
tools[0].FunctionDeclarations = make([]any, 0)
|
|
||||||
toolsResults := toolsResult.Array()
|
|
||||||
for i := 0; i < len(toolsResults); i++ {
|
|
||||||
toolResult := toolsResults[i]
|
|
||||||
inputSchemaResult := toolResult.Get("input_schema")
|
inputSchemaResult := toolResult.Get("input_schema")
|
||||||
if inputSchemaResult.Exists() && inputSchemaResult.IsObject() {
|
if inputSchemaResult.Exists() && inputSchemaResult.IsObject() {
|
||||||
inputSchema := inputSchemaResult.Raw
|
inputSchema := inputSchemaResult.Raw
|
||||||
@@ -136,30 +142,19 @@ func ConvertClaudeRequestToCLI(modelName string, inputRawJSON []byte, _ bool) []
|
|||||||
tool, _ = sjson.Delete(tool, "input_examples")
|
tool, _ = sjson.Delete(tool, "input_examples")
|
||||||
tool, _ = sjson.Delete(tool, "type")
|
tool, _ = sjson.Delete(tool, "type")
|
||||||
tool, _ = sjson.Delete(tool, "cache_control")
|
tool, _ = sjson.Delete(tool, "cache_control")
|
||||||
var toolDeclaration any
|
if gjson.Valid(tool) && gjson.Parse(tool).IsObject() {
|
||||||
if err := json.Unmarshal([]byte(tool), &toolDeclaration); err == nil {
|
if !hasTools {
|
||||||
tools[0].FunctionDeclarations = append(tools[0].FunctionDeclarations, toolDeclaration)
|
out, _ = sjson.SetRaw(out, "request.tools", `[{"functionDeclarations":[]}]`)
|
||||||
|
hasTools = true
|
||||||
|
}
|
||||||
|
out, _ = sjson.SetRaw(out, "request.tools.0.functionDeclarations.-1", tool)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
if !hasTools {
|
||||||
|
out, _ = sjson.Delete(out, "request.tools")
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
tools = make([]client.ToolDeclaration, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build output Gemini CLI request JSON
|
|
||||||
out := `{"model":"","request":{"contents":[]}}`
|
|
||||||
out, _ = sjson.Set(out, "model", modelName)
|
|
||||||
if systemInstruction != nil {
|
|
||||||
b, _ := json.Marshal(systemInstruction)
|
|
||||||
out, _ = sjson.SetRaw(out, "request.systemInstruction", string(b))
|
|
||||||
}
|
|
||||||
if len(contents) > 0 {
|
|
||||||
b, _ := json.Marshal(contents)
|
|
||||||
out, _ = sjson.SetRaw(out, "request.contents", string(b))
|
|
||||||
}
|
|
||||||
if len(tools) > 0 && len(tools[0].FunctionDeclarations) > 0 {
|
|
||||||
b, _ := json.Marshal(tools)
|
|
||||||
out, _ = sjson.SetRaw(out, "request.tools", string(b))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Map Anthropic thinking -> Gemini thinkingBudget/include_thoughts when type==enabled
|
// Map Anthropic thinking -> Gemini thinkingBudget/include_thoughts when type==enabled
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ package claude
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
@@ -276,22 +275,16 @@ func ConvertGeminiCLIResponseToClaudeNonStream(_ context.Context, _ string, orig
|
|||||||
|
|
||||||
root := gjson.ParseBytes(rawJSON)
|
root := gjson.ParseBytes(rawJSON)
|
||||||
|
|
||||||
response := map[string]interface{}{
|
out := `{"id":"","type":"message","role":"assistant","model":"","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":0,"output_tokens":0}}`
|
||||||
"id": root.Get("response.responseId").String(),
|
out, _ = sjson.Set(out, "id", root.Get("response.responseId").String())
|
||||||
"type": "message",
|
out, _ = sjson.Set(out, "model", root.Get("response.modelVersion").String())
|
||||||
"role": "assistant",
|
|
||||||
"model": root.Get("response.modelVersion").String(),
|
inputTokens := root.Get("response.usageMetadata.promptTokenCount").Int()
|
||||||
"content": []interface{}{},
|
outputTokens := root.Get("response.usageMetadata.candidatesTokenCount").Int() + root.Get("response.usageMetadata.thoughtsTokenCount").Int()
|
||||||
"stop_reason": nil,
|
out, _ = sjson.Set(out, "usage.input_tokens", inputTokens)
|
||||||
"stop_sequence": nil,
|
out, _ = sjson.Set(out, "usage.output_tokens", outputTokens)
|
||||||
"usage": map[string]interface{}{
|
|
||||||
"input_tokens": root.Get("response.usageMetadata.promptTokenCount").Int(),
|
|
||||||
"output_tokens": root.Get("response.usageMetadata.candidatesTokenCount").Int() + root.Get("response.usageMetadata.thoughtsTokenCount").Int(),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
parts := root.Get("response.candidates.0.content.parts")
|
parts := root.Get("response.candidates.0.content.parts")
|
||||||
var contentBlocks []interface{}
|
|
||||||
textBuilder := strings.Builder{}
|
textBuilder := strings.Builder{}
|
||||||
thinkingBuilder := strings.Builder{}
|
thinkingBuilder := strings.Builder{}
|
||||||
toolIDCounter := 0
|
toolIDCounter := 0
|
||||||
@@ -301,10 +294,9 @@ func ConvertGeminiCLIResponseToClaudeNonStream(_ context.Context, _ string, orig
|
|||||||
if textBuilder.Len() == 0 {
|
if textBuilder.Len() == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
contentBlocks = append(contentBlocks, map[string]interface{}{
|
block := `{"type":"text","text":""}`
|
||||||
"type": "text",
|
block, _ = sjson.Set(block, "text", textBuilder.String())
|
||||||
"text": textBuilder.String(),
|
out, _ = sjson.SetRaw(out, "content.-1", block)
|
||||||
})
|
|
||||||
textBuilder.Reset()
|
textBuilder.Reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -312,10 +304,9 @@ func ConvertGeminiCLIResponseToClaudeNonStream(_ context.Context, _ string, orig
|
|||||||
if thinkingBuilder.Len() == 0 {
|
if thinkingBuilder.Len() == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
contentBlocks = append(contentBlocks, map[string]interface{}{
|
block := `{"type":"thinking","thinking":""}`
|
||||||
"type": "thinking",
|
block, _ = sjson.Set(block, "thinking", thinkingBuilder.String())
|
||||||
"thinking": thinkingBuilder.String(),
|
out, _ = sjson.SetRaw(out, "content.-1", block)
|
||||||
})
|
|
||||||
thinkingBuilder.Reset()
|
thinkingBuilder.Reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -339,21 +330,15 @@ func ConvertGeminiCLIResponseToClaudeNonStream(_ context.Context, _ string, orig
|
|||||||
|
|
||||||
name := functionCall.Get("name").String()
|
name := functionCall.Get("name").String()
|
||||||
toolIDCounter++
|
toolIDCounter++
|
||||||
toolBlock := map[string]interface{}{
|
toolBlock := `{"type":"tool_use","id":"","name":"","input":{}}`
|
||||||
"type": "tool_use",
|
toolBlock, _ = sjson.Set(toolBlock, "id", fmt.Sprintf("tool_%d", toolIDCounter))
|
||||||
"id": fmt.Sprintf("tool_%d", toolIDCounter),
|
toolBlock, _ = sjson.Set(toolBlock, "name", name)
|
||||||
"name": name,
|
inputRaw := "{}"
|
||||||
"input": map[string]interface{}{},
|
if args := functionCall.Get("args"); args.Exists() && gjson.Valid(args.Raw) && args.IsObject() {
|
||||||
|
inputRaw = args.Raw
|
||||||
}
|
}
|
||||||
|
toolBlock, _ = sjson.SetRaw(toolBlock, "input", inputRaw)
|
||||||
if args := functionCall.Get("args"); args.Exists() {
|
out, _ = sjson.SetRaw(out, "content.-1", toolBlock)
|
||||||
var parsed interface{}
|
|
||||||
if err := json.Unmarshal([]byte(args.Raw), &parsed); err == nil {
|
|
||||||
toolBlock["input"] = parsed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
contentBlocks = append(contentBlocks, toolBlock)
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -362,8 +347,6 @@ func ConvertGeminiCLIResponseToClaudeNonStream(_ context.Context, _ string, orig
|
|||||||
flushThinking()
|
flushThinking()
|
||||||
flushText()
|
flushText()
|
||||||
|
|
||||||
response["content"] = contentBlocks
|
|
||||||
|
|
||||||
stopReason := "end_turn"
|
stopReason := "end_turn"
|
||||||
if hasToolCall {
|
if hasToolCall {
|
||||||
stopReason = "tool_use"
|
stopReason = "tool_use"
|
||||||
@@ -379,19 +362,13 @@ func ConvertGeminiCLIResponseToClaudeNonStream(_ context.Context, _ string, orig
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
response["stop_reason"] = stopReason
|
out, _ = sjson.Set(out, "stop_reason", stopReason)
|
||||||
|
|
||||||
if usage := response["usage"].(map[string]interface{}); usage["input_tokens"] == int64(0) && usage["output_tokens"] == int64(0) {
|
if inputTokens == int64(0) && outputTokens == int64(0) && !root.Get("response.usageMetadata").Exists() {
|
||||||
if usageMeta := root.Get("response.usageMetadata"); !usageMeta.Exists() {
|
out, _ = sjson.Delete(out, "usage")
|
||||||
delete(response, "usage")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
encoded, err := json.Marshal(response)
|
return out
|
||||||
if err != nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return string(encoded)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func ClaudeTokenCount(ctx context.Context, count int64) string {
|
func ClaudeTokenCount(ctx context.Context, count int64) string {
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ package gemini
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini/common"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini/common"
|
||||||
@@ -117,8 +116,6 @@ func ConvertGeminiRequestToGeminiCLI(_ string, inputRawJSON []byte, _ bool) []by
|
|||||||
|
|
||||||
// FunctionCallGroup represents a group of function calls and their responses
|
// FunctionCallGroup represents a group of function calls and their responses
|
||||||
type FunctionCallGroup struct {
|
type FunctionCallGroup struct {
|
||||||
ModelContent map[string]interface{}
|
|
||||||
FunctionCalls []gjson.Result
|
|
||||||
ResponsesNeeded int
|
ResponsesNeeded int
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,7 +143,7 @@ func fixCLIToolResponse(input string) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Initialize data structures for processing and grouping
|
// Initialize data structures for processing and grouping
|
||||||
var newContents []interface{} // Final processed contents array
|
contentsWrapper := `{"contents":[]}`
|
||||||
var pendingGroups []*FunctionCallGroup // Groups awaiting completion with responses
|
var pendingGroups []*FunctionCallGroup // Groups awaiting completion with responses
|
||||||
var collectedResponses []gjson.Result // Standalone responses to be matched
|
var collectedResponses []gjson.Result // Standalone responses to be matched
|
||||||
|
|
||||||
@@ -178,23 +175,17 @@ func fixCLIToolResponse(input string) (string, error) {
|
|||||||
collectedResponses = collectedResponses[group.ResponsesNeeded:]
|
collectedResponses = collectedResponses[group.ResponsesNeeded:]
|
||||||
|
|
||||||
// Create merged function response content
|
// Create merged function response content
|
||||||
var responseParts []interface{}
|
functionResponseContent := `{"parts":[],"role":"function"}`
|
||||||
for _, response := range groupResponses {
|
for _, response := range groupResponses {
|
||||||
var responseMap map[string]interface{}
|
if !response.IsObject() {
|
||||||
errUnmarshal := json.Unmarshal([]byte(response.Raw), &responseMap)
|
log.Warnf("failed to parse function response")
|
||||||
if errUnmarshal != nil {
|
|
||||||
log.Warnf("failed to unmarshal function response: %v\n", errUnmarshal)
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
responseParts = append(responseParts, responseMap)
|
functionResponseContent, _ = sjson.SetRaw(functionResponseContent, "parts.-1", response.Raw)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(responseParts) > 0 {
|
if gjson.Get(functionResponseContent, "parts.#").Int() > 0 {
|
||||||
functionResponseContent := map[string]interface{}{
|
contentsWrapper, _ = sjson.SetRaw(contentsWrapper, "contents.-1", functionResponseContent)
|
||||||
"parts": responseParts,
|
|
||||||
"role": "function",
|
|
||||||
}
|
|
||||||
newContents = append(newContents, functionResponseContent)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove this group as it's been satisfied
|
// Remove this group as it's been satisfied
|
||||||
@@ -208,50 +199,42 @@ func fixCLIToolResponse(input string) (string, error) {
|
|||||||
|
|
||||||
// If this is a model with function calls, create a new group
|
// If this is a model with function calls, create a new group
|
||||||
if role == "model" {
|
if role == "model" {
|
||||||
var functionCallsInThisModel []gjson.Result
|
functionCallsCount := 0
|
||||||
parts.ForEach(func(_, part gjson.Result) bool {
|
parts.ForEach(func(_, part gjson.Result) bool {
|
||||||
if part.Get("functionCall").Exists() {
|
if part.Get("functionCall").Exists() {
|
||||||
functionCallsInThisModel = append(functionCallsInThisModel, part)
|
functionCallsCount++
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
|
|
||||||
if len(functionCallsInThisModel) > 0 {
|
if functionCallsCount > 0 {
|
||||||
// Add the model content
|
// Add the model content
|
||||||
var contentMap map[string]interface{}
|
if !value.IsObject() {
|
||||||
errUnmarshal := json.Unmarshal([]byte(value.Raw), &contentMap)
|
log.Warnf("failed to parse model content")
|
||||||
if errUnmarshal != nil {
|
|
||||||
log.Warnf("failed to unmarshal model content: %v\n", errUnmarshal)
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
newContents = append(newContents, contentMap)
|
contentsWrapper, _ = sjson.SetRaw(contentsWrapper, "contents.-1", value.Raw)
|
||||||
|
|
||||||
// Create a new group for tracking responses
|
// Create a new group for tracking responses
|
||||||
group := &FunctionCallGroup{
|
group := &FunctionCallGroup{
|
||||||
ModelContent: contentMap,
|
ResponsesNeeded: functionCallsCount,
|
||||||
FunctionCalls: functionCallsInThisModel,
|
|
||||||
ResponsesNeeded: len(functionCallsInThisModel),
|
|
||||||
}
|
}
|
||||||
pendingGroups = append(pendingGroups, group)
|
pendingGroups = append(pendingGroups, group)
|
||||||
} else {
|
} else {
|
||||||
// Regular model content without function calls
|
// Regular model content without function calls
|
||||||
var contentMap map[string]interface{}
|
if !value.IsObject() {
|
||||||
errUnmarshal := json.Unmarshal([]byte(value.Raw), &contentMap)
|
log.Warnf("failed to parse content")
|
||||||
if errUnmarshal != nil {
|
|
||||||
log.Warnf("failed to unmarshal content: %v\n", errUnmarshal)
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
newContents = append(newContents, contentMap)
|
contentsWrapper, _ = sjson.SetRaw(contentsWrapper, "contents.-1", value.Raw)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Non-model content (user, etc.)
|
// Non-model content (user, etc.)
|
||||||
var contentMap map[string]interface{}
|
if !value.IsObject() {
|
||||||
errUnmarshal := json.Unmarshal([]byte(value.Raw), &contentMap)
|
log.Warnf("failed to parse content")
|
||||||
if errUnmarshal != nil {
|
|
||||||
log.Warnf("failed to unmarshal content: %v\n", errUnmarshal)
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
newContents = append(newContents, contentMap)
|
contentsWrapper, _ = sjson.SetRaw(contentsWrapper, "contents.-1", value.Raw)
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
@@ -263,31 +246,24 @@ func fixCLIToolResponse(input string) (string, error) {
|
|||||||
groupResponses := collectedResponses[:group.ResponsesNeeded]
|
groupResponses := collectedResponses[:group.ResponsesNeeded]
|
||||||
collectedResponses = collectedResponses[group.ResponsesNeeded:]
|
collectedResponses = collectedResponses[group.ResponsesNeeded:]
|
||||||
|
|
||||||
var responseParts []interface{}
|
functionResponseContent := `{"parts":[],"role":"function"}`
|
||||||
for _, response := range groupResponses {
|
for _, response := range groupResponses {
|
||||||
var responseMap map[string]interface{}
|
if !response.IsObject() {
|
||||||
errUnmarshal := json.Unmarshal([]byte(response.Raw), &responseMap)
|
log.Warnf("failed to parse function response")
|
||||||
if errUnmarshal != nil {
|
|
||||||
log.Warnf("failed to unmarshal function response: %v\n", errUnmarshal)
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
responseParts = append(responseParts, responseMap)
|
functionResponseContent, _ = sjson.SetRaw(functionResponseContent, "parts.-1", response.Raw)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(responseParts) > 0 {
|
if gjson.Get(functionResponseContent, "parts.#").Int() > 0 {
|
||||||
functionResponseContent := map[string]interface{}{
|
contentsWrapper, _ = sjson.SetRaw(contentsWrapper, "contents.-1", functionResponseContent)
|
||||||
"parts": responseParts,
|
|
||||||
"role": "function",
|
|
||||||
}
|
|
||||||
newContents = append(newContents, functionResponseContent)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the original JSON with the new contents
|
// Update the original JSON with the new contents
|
||||||
result := input
|
result := input
|
||||||
newContentsJSON, _ := json.Marshal(newContents)
|
result, _ = sjson.SetRaw(result, "request.contents", gjson.Get(contentsWrapper, "contents").Raw)
|
||||||
result, _ = sjson.Set(result, "request.contents", json.RawMessage(newContentsJSON))
|
|
||||||
|
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -278,7 +278,7 @@ func ConvertOpenAIRequestToGeminiCLI(modelName string, inputRawJSON []byte, _ bo
|
|||||||
log.Warnf("Failed to set default schema type for tool '%s': %v", fn.Get("name").String(), errSet)
|
log.Warnf("Failed to set default schema type for tool '%s': %v", fn.Get("name").String(), errSet)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
fnRaw, errSet = sjson.Set(fnRaw, "parametersJsonSchema.properties", map[string]interface{}{})
|
fnRaw, errSet = sjson.SetRaw(fnRaw, "parametersJsonSchema.properties", `{}`)
|
||||||
if errSet != nil {
|
if errSet != nil {
|
||||||
log.Warnf("Failed to set default schema properties for tool '%s': %v", fn.Get("name").String(), errSet)
|
log.Warnf("Failed to set default schema properties for tool '%s': %v", fn.Get("name").String(), errSet)
|
||||||
continue
|
continue
|
||||||
@@ -293,7 +293,7 @@ func ConvertOpenAIRequestToGeminiCLI(modelName string, inputRawJSON []byte, _ bo
|
|||||||
log.Warnf("Failed to set default schema type for tool '%s': %v", fn.Get("name").String(), errSet)
|
log.Warnf("Failed to set default schema type for tool '%s': %v", fn.Get("name").String(), errSet)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
fnRaw, errSet = sjson.Set(fnRaw, "parametersJsonSchema.properties", map[string]interface{}{})
|
fnRaw, errSet = sjson.SetRaw(fnRaw, "parametersJsonSchema.properties", `{}`)
|
||||||
if errSet != nil {
|
if errSet != nil {
|
||||||
log.Warnf("Failed to set default schema properties for tool '%s': %v", fn.Get("name").String(), errSet)
|
log.Warnf("Failed to set default schema properties for tool '%s': %v", fn.Get("name").String(), errSet)
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ package chat_completions
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
@@ -171,21 +170,14 @@ func ConvertCliResponseToOpenAI(_ context.Context, _ string, originalRequestRawJ
|
|||||||
mimeType = "image/png"
|
mimeType = "image/png"
|
||||||
}
|
}
|
||||||
imageURL := fmt.Sprintf("data:%s;base64,%s", mimeType, data)
|
imageURL := fmt.Sprintf("data:%s;base64,%s", mimeType, data)
|
||||||
imagePayload, err := json.Marshal(map[string]any{
|
imagePayload := `{"image_url":{"url":""},"type":"image_url"}`
|
||||||
"type": "image_url",
|
imagePayload, _ = sjson.Set(imagePayload, "image_url.url", imageURL)
|
||||||
"image_url": map[string]string{
|
|
||||||
"url": imageURL,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
imagesResult := gjson.Get(template, "choices.0.delta.images")
|
imagesResult := gjson.Get(template, "choices.0.delta.images")
|
||||||
if !imagesResult.Exists() || !imagesResult.IsArray() {
|
if !imagesResult.Exists() || !imagesResult.IsArray() {
|
||||||
template, _ = sjson.SetRaw(template, "choices.0.delta.images", `[]`)
|
template, _ = sjson.SetRaw(template, "choices.0.delta.images", `[]`)
|
||||||
}
|
}
|
||||||
template, _ = sjson.Set(template, "choices.0.delta.role", "assistant")
|
template, _ = sjson.Set(template, "choices.0.delta.role", "assistant")
|
||||||
template, _ = sjson.SetRaw(template, "choices.0.delta.images.-1", string(imagePayload))
|
template, _ = sjson.SetRaw(template, "choices.0.delta.images.-1", imagePayload)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,10 +7,8 @@ package claude
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
client "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces"
|
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini/common"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini/common"
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
|
||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
@@ -34,92 +32,100 @@ func ConvertClaudeRequestToGemini(modelName string, inputRawJSON []byte, _ bool)
|
|||||||
rawJSON := bytes.Clone(inputRawJSON)
|
rawJSON := bytes.Clone(inputRawJSON)
|
||||||
rawJSON = bytes.Replace(rawJSON, []byte(`"url":{"type":"string","format":"uri",`), []byte(`"url":{"type":"string",`), -1)
|
rawJSON = bytes.Replace(rawJSON, []byte(`"url":{"type":"string","format":"uri",`), []byte(`"url":{"type":"string",`), -1)
|
||||||
|
|
||||||
|
// Build output Gemini CLI request JSON
|
||||||
|
out := `{"contents":[]}`
|
||||||
|
out, _ = sjson.Set(out, "model", modelName)
|
||||||
|
|
||||||
// system instruction
|
// system instruction
|
||||||
var systemInstruction *client.Content
|
if systemResult := gjson.GetBytes(rawJSON, "system"); systemResult.IsArray() {
|
||||||
systemResult := gjson.GetBytes(rawJSON, "system")
|
systemInstruction := `{"role":"user","parts":[]}`
|
||||||
if systemResult.IsArray() {
|
hasSystemParts := false
|
||||||
systemResults := systemResult.Array()
|
systemResult.ForEach(func(_, systemPromptResult gjson.Result) bool {
|
||||||
systemInstruction = &client.Content{Role: "user", Parts: []client.Part{}}
|
if systemPromptResult.Get("type").String() == "text" {
|
||||||
for i := 0; i < len(systemResults); i++ {
|
textResult := systemPromptResult.Get("text")
|
||||||
systemPromptResult := systemResults[i]
|
if textResult.Type == gjson.String {
|
||||||
systemTypePromptResult := systemPromptResult.Get("type")
|
part := `{"text":""}`
|
||||||
if systemTypePromptResult.Type == gjson.String && systemTypePromptResult.String() == "text" {
|
part, _ = sjson.Set(part, "text", textResult.String())
|
||||||
systemPrompt := systemPromptResult.Get("text").String()
|
systemInstruction, _ = sjson.SetRaw(systemInstruction, "parts.-1", part)
|
||||||
systemPart := client.Part{Text: systemPrompt}
|
hasSystemParts = true
|
||||||
systemInstruction.Parts = append(systemInstruction.Parts, systemPart)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(systemInstruction.Parts) == 0 {
|
return true
|
||||||
systemInstruction = nil
|
})
|
||||||
|
if hasSystemParts {
|
||||||
|
out, _ = sjson.SetRaw(out, "system_instruction", systemInstruction)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// contents
|
// contents
|
||||||
contents := make([]client.Content, 0)
|
if messagesResult := gjson.GetBytes(rawJSON, "messages"); messagesResult.IsArray() {
|
||||||
messagesResult := gjson.GetBytes(rawJSON, "messages")
|
messagesResult.ForEach(func(_, messageResult gjson.Result) bool {
|
||||||
if messagesResult.IsArray() {
|
|
||||||
messageResults := messagesResult.Array()
|
|
||||||
for i := 0; i < len(messageResults); i++ {
|
|
||||||
messageResult := messageResults[i]
|
|
||||||
roleResult := messageResult.Get("role")
|
roleResult := messageResult.Get("role")
|
||||||
if roleResult.Type != gjson.String {
|
if roleResult.Type != gjson.String {
|
||||||
continue
|
return true
|
||||||
}
|
}
|
||||||
role := roleResult.String()
|
role := roleResult.String()
|
||||||
if role == "assistant" {
|
if role == "assistant" {
|
||||||
role = "model"
|
role = "model"
|
||||||
}
|
}
|
||||||
clientContent := client.Content{Role: role, Parts: []client.Part{}}
|
|
||||||
|
contentJSON := `{"role":"","parts":[]}`
|
||||||
|
contentJSON, _ = sjson.Set(contentJSON, "role", role)
|
||||||
|
|
||||||
contentsResult := messageResult.Get("content")
|
contentsResult := messageResult.Get("content")
|
||||||
if contentsResult.IsArray() {
|
if contentsResult.IsArray() {
|
||||||
contentResults := contentsResult.Array()
|
contentsResult.ForEach(func(_, contentResult gjson.Result) bool {
|
||||||
for j := 0; j < len(contentResults); j++ {
|
switch contentResult.Get("type").String() {
|
||||||
contentResult := contentResults[j]
|
case "text":
|
||||||
contentTypeResult := contentResult.Get("type")
|
part := `{"text":""}`
|
||||||
if contentTypeResult.Type == gjson.String && contentTypeResult.String() == "text" {
|
part, _ = sjson.Set(part, "text", contentResult.Get("text").String())
|
||||||
prompt := contentResult.Get("text").String()
|
contentJSON, _ = sjson.SetRaw(contentJSON, "parts.-1", part)
|
||||||
clientContent.Parts = append(clientContent.Parts, client.Part{Text: prompt})
|
|
||||||
} else if contentTypeResult.Type == gjson.String && contentTypeResult.String() == "tool_use" {
|
case "tool_use":
|
||||||
functionName := contentResult.Get("name").String()
|
functionName := contentResult.Get("name").String()
|
||||||
functionArgs := contentResult.Get("input").String()
|
functionArgs := contentResult.Get("input").String()
|
||||||
var args map[string]any
|
argsResult := gjson.Parse(functionArgs)
|
||||||
if err := json.Unmarshal([]byte(functionArgs), &args); err == nil {
|
if argsResult.IsObject() && gjson.Valid(functionArgs) {
|
||||||
clientContent.Parts = append(clientContent.Parts, client.Part{
|
part := `{"thoughtSignature":"","functionCall":{"name":"","args":{}}}`
|
||||||
FunctionCall: &client.FunctionCall{Name: functionName, Args: args},
|
part, _ = sjson.Set(part, "thoughtSignature", geminiClaudeThoughtSignature)
|
||||||
ThoughtSignature: geminiClaudeThoughtSignature,
|
part, _ = sjson.Set(part, "functionCall.name", functionName)
|
||||||
})
|
part, _ = sjson.SetRaw(part, "functionCall.args", functionArgs)
|
||||||
|
contentJSON, _ = sjson.SetRaw(contentJSON, "parts.-1", part)
|
||||||
}
|
}
|
||||||
} else if contentTypeResult.Type == gjson.String && contentTypeResult.String() == "tool_result" {
|
|
||||||
|
case "tool_result":
|
||||||
toolCallID := contentResult.Get("tool_use_id").String()
|
toolCallID := contentResult.Get("tool_use_id").String()
|
||||||
if toolCallID != "" {
|
if toolCallID == "" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
funcName := toolCallID
|
funcName := toolCallID
|
||||||
toolCallIDs := strings.Split(toolCallID, "-")
|
toolCallIDs := strings.Split(toolCallID, "-")
|
||||||
if len(toolCallIDs) > 1 {
|
if len(toolCallIDs) > 1 {
|
||||||
funcName = strings.Join(toolCallIDs[0:len(toolCallIDs)-1], "-")
|
funcName = strings.Join(toolCallIDs[0:len(toolCallIDs)-1], "-")
|
||||||
}
|
}
|
||||||
responseData := contentResult.Get("content").Raw
|
responseData := contentResult.Get("content").Raw
|
||||||
functionResponse := client.FunctionResponse{Name: funcName, Response: map[string]interface{}{"result": responseData}}
|
part := `{"functionResponse":{"name":"","response":{"result":""}}}`
|
||||||
clientContent.Parts = append(clientContent.Parts, client.Part{FunctionResponse: &functionResponse})
|
part, _ = sjson.Set(part, "functionResponse.name", funcName)
|
||||||
|
part, _ = sjson.Set(part, "functionResponse.response.result", responseData)
|
||||||
|
contentJSON, _ = sjson.SetRaw(contentJSON, "parts.-1", part)
|
||||||
}
|
}
|
||||||
}
|
return true
|
||||||
}
|
})
|
||||||
contents = append(contents, clientContent)
|
out, _ = sjson.SetRaw(out, "contents.-1", contentJSON)
|
||||||
} else if contentsResult.Type == gjson.String {
|
} else if contentsResult.Type == gjson.String {
|
||||||
prompt := contentsResult.String()
|
part := `{"text":""}`
|
||||||
contents = append(contents, client.Content{Role: role, Parts: []client.Part{{Text: prompt}}})
|
part, _ = sjson.Set(part, "text", contentsResult.String())
|
||||||
}
|
contentJSON, _ = sjson.SetRaw(contentJSON, "parts.-1", part)
|
||||||
|
out, _ = sjson.SetRaw(out, "contents.-1", contentJSON)
|
||||||
}
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// tools
|
// tools
|
||||||
var tools []client.ToolDeclaration
|
if toolsResult := gjson.GetBytes(rawJSON, "tools"); toolsResult.IsArray() {
|
||||||
toolsResult := gjson.GetBytes(rawJSON, "tools")
|
hasTools := false
|
||||||
if toolsResult.IsArray() {
|
toolsResult.ForEach(func(_, toolResult gjson.Result) bool {
|
||||||
tools = make([]client.ToolDeclaration, 1)
|
|
||||||
tools[0].FunctionDeclarations = make([]any, 0)
|
|
||||||
toolsResults := toolsResult.Array()
|
|
||||||
for i := 0; i < len(toolsResults); i++ {
|
|
||||||
toolResult := toolsResults[i]
|
|
||||||
inputSchemaResult := toolResult.Get("input_schema")
|
inputSchemaResult := toolResult.Get("input_schema")
|
||||||
if inputSchemaResult.Exists() && inputSchemaResult.IsObject() {
|
if inputSchemaResult.Exists() && inputSchemaResult.IsObject() {
|
||||||
inputSchema := inputSchemaResult.Raw
|
inputSchema := inputSchemaResult.Raw
|
||||||
@@ -129,30 +135,19 @@ func ConvertClaudeRequestToGemini(modelName string, inputRawJSON []byte, _ bool)
|
|||||||
tool, _ = sjson.Delete(tool, "input_examples")
|
tool, _ = sjson.Delete(tool, "input_examples")
|
||||||
tool, _ = sjson.Delete(tool, "type")
|
tool, _ = sjson.Delete(tool, "type")
|
||||||
tool, _ = sjson.Delete(tool, "cache_control")
|
tool, _ = sjson.Delete(tool, "cache_control")
|
||||||
var toolDeclaration any
|
if gjson.Valid(tool) && gjson.Parse(tool).IsObject() {
|
||||||
if err := json.Unmarshal([]byte(tool), &toolDeclaration); err == nil {
|
if !hasTools {
|
||||||
tools[0].FunctionDeclarations = append(tools[0].FunctionDeclarations, toolDeclaration)
|
out, _ = sjson.SetRaw(out, "tools", `[{"functionDeclarations":[]}]`)
|
||||||
|
hasTools = true
|
||||||
|
}
|
||||||
|
out, _ = sjson.SetRaw(out, "tools.0.functionDeclarations.-1", tool)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
if !hasTools {
|
||||||
|
out, _ = sjson.Delete(out, "tools")
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
tools = make([]client.ToolDeclaration, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build output Gemini CLI request JSON
|
|
||||||
out := `{"contents":[]}`
|
|
||||||
out, _ = sjson.Set(out, "model", modelName)
|
|
||||||
if systemInstruction != nil {
|
|
||||||
b, _ := json.Marshal(systemInstruction)
|
|
||||||
out, _ = sjson.SetRaw(out, "system_instruction", string(b))
|
|
||||||
}
|
|
||||||
if len(contents) > 0 {
|
|
||||||
b, _ := json.Marshal(contents)
|
|
||||||
out, _ = sjson.SetRaw(out, "contents", string(b))
|
|
||||||
}
|
|
||||||
if len(tools) > 0 && len(tools[0].FunctionDeclarations) > 0 {
|
|
||||||
b, _ := json.Marshal(tools)
|
|
||||||
out, _ = sjson.SetRaw(out, "tools", string(b))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Map Anthropic thinking -> Gemini thinkingBudget/include_thoughts when enabled
|
// Map Anthropic thinking -> Gemini thinkingBudget/include_thoughts when enabled
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ package claude
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
@@ -282,22 +281,16 @@ func ConvertGeminiResponseToClaudeNonStream(_ context.Context, _ string, origina
|
|||||||
|
|
||||||
root := gjson.ParseBytes(rawJSON)
|
root := gjson.ParseBytes(rawJSON)
|
||||||
|
|
||||||
response := map[string]interface{}{
|
out := `{"id":"","type":"message","role":"assistant","model":"","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":0,"output_tokens":0}}`
|
||||||
"id": root.Get("responseId").String(),
|
out, _ = sjson.Set(out, "id", root.Get("responseId").String())
|
||||||
"type": "message",
|
out, _ = sjson.Set(out, "model", root.Get("modelVersion").String())
|
||||||
"role": "assistant",
|
|
||||||
"model": root.Get("modelVersion").String(),
|
inputTokens := root.Get("usageMetadata.promptTokenCount").Int()
|
||||||
"content": []interface{}{},
|
outputTokens := root.Get("usageMetadata.candidatesTokenCount").Int() + root.Get("usageMetadata.thoughtsTokenCount").Int()
|
||||||
"stop_reason": nil,
|
out, _ = sjson.Set(out, "usage.input_tokens", inputTokens)
|
||||||
"stop_sequence": nil,
|
out, _ = sjson.Set(out, "usage.output_tokens", outputTokens)
|
||||||
"usage": map[string]interface{}{
|
|
||||||
"input_tokens": root.Get("usageMetadata.promptTokenCount").Int(),
|
|
||||||
"output_tokens": root.Get("usageMetadata.candidatesTokenCount").Int() + root.Get("usageMetadata.thoughtsTokenCount").Int(),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
parts := root.Get("candidates.0.content.parts")
|
parts := root.Get("candidates.0.content.parts")
|
||||||
var contentBlocks []interface{}
|
|
||||||
textBuilder := strings.Builder{}
|
textBuilder := strings.Builder{}
|
||||||
thinkingBuilder := strings.Builder{}
|
thinkingBuilder := strings.Builder{}
|
||||||
toolIDCounter := 0
|
toolIDCounter := 0
|
||||||
@@ -307,10 +300,9 @@ func ConvertGeminiResponseToClaudeNonStream(_ context.Context, _ string, origina
|
|||||||
if textBuilder.Len() == 0 {
|
if textBuilder.Len() == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
contentBlocks = append(contentBlocks, map[string]interface{}{
|
block := `{"type":"text","text":""}`
|
||||||
"type": "text",
|
block, _ = sjson.Set(block, "text", textBuilder.String())
|
||||||
"text": textBuilder.String(),
|
out, _ = sjson.SetRaw(out, "content.-1", block)
|
||||||
})
|
|
||||||
textBuilder.Reset()
|
textBuilder.Reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -318,10 +310,9 @@ func ConvertGeminiResponseToClaudeNonStream(_ context.Context, _ string, origina
|
|||||||
if thinkingBuilder.Len() == 0 {
|
if thinkingBuilder.Len() == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
contentBlocks = append(contentBlocks, map[string]interface{}{
|
block := `{"type":"thinking","thinking":""}`
|
||||||
"type": "thinking",
|
block, _ = sjson.Set(block, "thinking", thinkingBuilder.String())
|
||||||
"thinking": thinkingBuilder.String(),
|
out, _ = sjson.SetRaw(out, "content.-1", block)
|
||||||
})
|
|
||||||
thinkingBuilder.Reset()
|
thinkingBuilder.Reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -345,21 +336,15 @@ func ConvertGeminiResponseToClaudeNonStream(_ context.Context, _ string, origina
|
|||||||
|
|
||||||
name := functionCall.Get("name").String()
|
name := functionCall.Get("name").String()
|
||||||
toolIDCounter++
|
toolIDCounter++
|
||||||
toolBlock := map[string]interface{}{
|
toolBlock := `{"type":"tool_use","id":"","name":"","input":{}}`
|
||||||
"type": "tool_use",
|
toolBlock, _ = sjson.Set(toolBlock, "id", fmt.Sprintf("tool_%d", toolIDCounter))
|
||||||
"id": fmt.Sprintf("tool_%d", toolIDCounter),
|
toolBlock, _ = sjson.Set(toolBlock, "name", name)
|
||||||
"name": name,
|
inputRaw := "{}"
|
||||||
"input": map[string]interface{}{},
|
if args := functionCall.Get("args"); args.Exists() && gjson.Valid(args.Raw) && args.IsObject() {
|
||||||
|
inputRaw = args.Raw
|
||||||
}
|
}
|
||||||
|
toolBlock, _ = sjson.SetRaw(toolBlock, "input", inputRaw)
|
||||||
if args := functionCall.Get("args"); args.Exists() {
|
out, _ = sjson.SetRaw(out, "content.-1", toolBlock)
|
||||||
var parsed interface{}
|
|
||||||
if err := json.Unmarshal([]byte(args.Raw), &parsed); err == nil {
|
|
||||||
toolBlock["input"] = parsed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
contentBlocks = append(contentBlocks, toolBlock)
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -368,8 +353,6 @@ func ConvertGeminiResponseToClaudeNonStream(_ context.Context, _ string, origina
|
|||||||
flushThinking()
|
flushThinking()
|
||||||
flushText()
|
flushText()
|
||||||
|
|
||||||
response["content"] = contentBlocks
|
|
||||||
|
|
||||||
stopReason := "end_turn"
|
stopReason := "end_turn"
|
||||||
if hasToolCall {
|
if hasToolCall {
|
||||||
stopReason = "tool_use"
|
stopReason = "tool_use"
|
||||||
@@ -385,19 +368,13 @@ func ConvertGeminiResponseToClaudeNonStream(_ context.Context, _ string, origina
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
response["stop_reason"] = stopReason
|
out, _ = sjson.Set(out, "stop_reason", stopReason)
|
||||||
|
|
||||||
if usage := response["usage"].(map[string]interface{}); usage["input_tokens"] == int64(0) && usage["output_tokens"] == int64(0) {
|
if inputTokens == int64(0) && outputTokens == int64(0) && !root.Get("usageMetadata").Exists() {
|
||||||
if usageMeta := root.Get("usageMetadata"); !usageMeta.Exists() {
|
out, _ = sjson.Delete(out, "usage")
|
||||||
delete(response, "usage")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
encoded, err := json.Marshal(response)
|
return out
|
||||||
if err != nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return string(encoded)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func ClaudeTokenCount(ctx context.Context, count int64) string {
|
func ClaudeTokenCount(ctx context.Context, count int64) string {
|
||||||
|
|||||||
@@ -320,7 +320,7 @@ func ConvertOpenAIRequestToGemini(modelName string, inputRawJSON []byte, _ bool)
|
|||||||
log.Warnf("Failed to set default schema type for tool '%s': %v", fn.Get("name").String(), errSet)
|
log.Warnf("Failed to set default schema type for tool '%s': %v", fn.Get("name").String(), errSet)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
fnRaw, errSet = sjson.Set(fnRaw, "parametersJsonSchema.properties", map[string]interface{}{})
|
fnRaw, errSet = sjson.SetRaw(fnRaw, "parametersJsonSchema.properties", `{}`)
|
||||||
if errSet != nil {
|
if errSet != nil {
|
||||||
log.Warnf("Failed to set default schema properties for tool '%s': %v", fn.Get("name").String(), errSet)
|
log.Warnf("Failed to set default schema properties for tool '%s': %v", fn.Get("name").String(), errSet)
|
||||||
continue
|
continue
|
||||||
@@ -335,7 +335,7 @@ func ConvertOpenAIRequestToGemini(modelName string, inputRawJSON []byte, _ bool)
|
|||||||
log.Warnf("Failed to set default schema type for tool '%s': %v", fn.Get("name").String(), errSet)
|
log.Warnf("Failed to set default schema type for tool '%s': %v", fn.Get("name").String(), errSet)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
fnRaw, errSet = sjson.Set(fnRaw, "parametersJsonSchema.properties", map[string]interface{}{})
|
fnRaw, errSet = sjson.SetRaw(fnRaw, "parametersJsonSchema.properties", `{}`)
|
||||||
if errSet != nil {
|
if errSet != nil {
|
||||||
log.Warnf("Failed to set default schema properties for tool '%s': %v", fn.Get("name").String(), errSet)
|
log.Warnf("Failed to set default schema properties for tool '%s': %v", fn.Get("name").String(), errSet)
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ package chat_completions
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
@@ -173,21 +172,14 @@ func ConvertGeminiResponseToOpenAI(_ context.Context, _ string, originalRequestR
|
|||||||
mimeType = "image/png"
|
mimeType = "image/png"
|
||||||
}
|
}
|
||||||
imageURL := fmt.Sprintf("data:%s;base64,%s", mimeType, data)
|
imageURL := fmt.Sprintf("data:%s;base64,%s", mimeType, data)
|
||||||
imagePayload, err := json.Marshal(map[string]any{
|
imagePayload := `{"image_url":{"url":""},"type":"image_url"}`
|
||||||
"type": "image_url",
|
imagePayload, _ = sjson.Set(imagePayload, "image_url.url", imageURL)
|
||||||
"image_url": map[string]string{
|
|
||||||
"url": imageURL,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
imagesResult := gjson.Get(template, "choices.0.delta.images")
|
imagesResult := gjson.Get(template, "choices.0.delta.images")
|
||||||
if !imagesResult.Exists() || !imagesResult.IsArray() {
|
if !imagesResult.Exists() || !imagesResult.IsArray() {
|
||||||
template, _ = sjson.SetRaw(template, "choices.0.delta.images", `[]`)
|
template, _ = sjson.SetRaw(template, "choices.0.delta.images", `[]`)
|
||||||
}
|
}
|
||||||
template, _ = sjson.Set(template, "choices.0.delta.role", "assistant")
|
template, _ = sjson.Set(template, "choices.0.delta.role", "assistant")
|
||||||
template, _ = sjson.SetRaw(template, "choices.0.delta.images.-1", string(imagePayload))
|
template, _ = sjson.SetRaw(template, "choices.0.delta.images.-1", imagePayload)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -305,21 +297,14 @@ func ConvertGeminiResponseToOpenAINonStream(_ context.Context, _ string, origina
|
|||||||
mimeType = "image/png"
|
mimeType = "image/png"
|
||||||
}
|
}
|
||||||
imageURL := fmt.Sprintf("data:%s;base64,%s", mimeType, data)
|
imageURL := fmt.Sprintf("data:%s;base64,%s", mimeType, data)
|
||||||
imagePayload, err := json.Marshal(map[string]any{
|
imagePayload := `{"image_url":{"url":""},"type":"image_url"}`
|
||||||
"type": "image_url",
|
imagePayload, _ = sjson.Set(imagePayload, "image_url.url", imageURL)
|
||||||
"image_url": map[string]string{
|
|
||||||
"url": imageURL,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
imagesResult := gjson.Get(template, "choices.0.message.images")
|
imagesResult := gjson.Get(template, "choices.0.message.images")
|
||||||
if !imagesResult.Exists() || !imagesResult.IsArray() {
|
if !imagesResult.Exists() || !imagesResult.IsArray() {
|
||||||
template, _ = sjson.SetRaw(template, "choices.0.message.images", `[]`)
|
template, _ = sjson.SetRaw(template, "choices.0.message.images", `[]`)
|
||||||
}
|
}
|
||||||
template, _ = sjson.Set(template, "choices.0.message.role", "assistant")
|
template, _ = sjson.Set(template, "choices.0.message.role", "assistant")
|
||||||
template, _ = sjson.SetRaw(template, "choices.0.message.images.-1", string(imagePayload))
|
template, _ = sjson.SetRaw(template, "choices.0.message.images.-1", imagePayload)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -377,27 +377,18 @@ func ConvertGeminiResponseToOpenAIResponses(_ context.Context, modelName string,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Compose outputs in encountered order: reasoning, message, function_calls
|
// Compose outputs in encountered order: reasoning, message, function_calls
|
||||||
var outputs []interface{}
|
outputsWrapper := `{"arr":[]}`
|
||||||
if st.ReasoningOpened {
|
if st.ReasoningOpened {
|
||||||
outputs = append(outputs, map[string]interface{}{
|
item := `{"id":"","type":"reasoning","summary":[{"type":"summary_text","text":""}]}`
|
||||||
"id": st.ReasoningItemID,
|
item, _ = sjson.Set(item, "id", st.ReasoningItemID)
|
||||||
"type": "reasoning",
|
item, _ = sjson.Set(item, "summary.0.text", st.ReasoningBuf.String())
|
||||||
"summary": []interface{}{map[string]interface{}{"type": "summary_text", "text": st.ReasoningBuf.String()}},
|
outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", item)
|
||||||
})
|
|
||||||
}
|
}
|
||||||
if st.MsgOpened {
|
if st.MsgOpened {
|
||||||
outputs = append(outputs, map[string]interface{}{
|
item := `{"id":"","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":""}],"role":"assistant"}`
|
||||||
"id": st.CurrentMsgID,
|
item, _ = sjson.Set(item, "id", st.CurrentMsgID)
|
||||||
"type": "message",
|
item, _ = sjson.Set(item, "content.0.text", st.TextBuf.String())
|
||||||
"status": "completed",
|
outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", item)
|
||||||
"content": []interface{}{map[string]interface{}{
|
|
||||||
"type": "output_text",
|
|
||||||
"annotations": []interface{}{},
|
|
||||||
"logprobs": []interface{}{},
|
|
||||||
"text": st.TextBuf.String(),
|
|
||||||
}},
|
|
||||||
"role": "assistant",
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
if len(st.FuncArgsBuf) > 0 {
|
if len(st.FuncArgsBuf) > 0 {
|
||||||
idxs := make([]int, 0, len(st.FuncArgsBuf))
|
idxs := make([]int, 0, len(st.FuncArgsBuf))
|
||||||
@@ -416,18 +407,16 @@ func ConvertGeminiResponseToOpenAIResponses(_ context.Context, modelName string,
|
|||||||
if b := st.FuncArgsBuf[idx]; b != nil {
|
if b := st.FuncArgsBuf[idx]; b != nil {
|
||||||
args = b.String()
|
args = b.String()
|
||||||
}
|
}
|
||||||
outputs = append(outputs, map[string]interface{}{
|
item := `{"id":"","type":"function_call","status":"completed","arguments":"","call_id":"","name":""}`
|
||||||
"id": fmt.Sprintf("fc_%s", st.FuncCallIDs[idx]),
|
item, _ = sjson.Set(item, "id", fmt.Sprintf("fc_%s", st.FuncCallIDs[idx]))
|
||||||
"type": "function_call",
|
item, _ = sjson.Set(item, "arguments", args)
|
||||||
"status": "completed",
|
item, _ = sjson.Set(item, "call_id", st.FuncCallIDs[idx])
|
||||||
"arguments": args,
|
item, _ = sjson.Set(item, "name", st.FuncNames[idx])
|
||||||
"call_id": st.FuncCallIDs[idx],
|
outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", item)
|
||||||
"name": st.FuncNames[idx],
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(outputs) > 0 {
|
if gjson.Get(outputsWrapper, "arr.#").Int() > 0 {
|
||||||
completed, _ = sjson.Set(completed, "response.output", outputs)
|
completed, _ = sjson.SetRaw(completed, "response.output", gjson.Get(outputsWrapper, "arr").Raw)
|
||||||
}
|
}
|
||||||
|
|
||||||
// usage mapping
|
// usage mapping
|
||||||
@@ -558,11 +547,24 @@ func ConvertGeminiResponseToOpenAIResponsesNonStream(_ context.Context, _ string
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Build outputs from candidates[0].content.parts
|
// Build outputs from candidates[0].content.parts
|
||||||
var outputs []interface{}
|
|
||||||
var reasoningText strings.Builder
|
var reasoningText strings.Builder
|
||||||
var reasoningEncrypted string
|
var reasoningEncrypted string
|
||||||
var messageText strings.Builder
|
var messageText strings.Builder
|
||||||
var haveMessage bool
|
var haveMessage bool
|
||||||
|
|
||||||
|
haveOutput := false
|
||||||
|
ensureOutput := func() {
|
||||||
|
if haveOutput {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, _ = sjson.SetRaw(resp, "output", "[]")
|
||||||
|
haveOutput = true
|
||||||
|
}
|
||||||
|
appendOutput := func(itemJSON string) {
|
||||||
|
ensureOutput()
|
||||||
|
resp, _ = sjson.SetRaw(resp, "output.-1", itemJSON)
|
||||||
|
}
|
||||||
|
|
||||||
if parts := root.Get("candidates.0.content.parts"); parts.Exists() && parts.IsArray() {
|
if parts := root.Get("candidates.0.content.parts"); parts.Exists() && parts.IsArray() {
|
||||||
parts.ForEach(func(_, p gjson.Result) bool {
|
parts.ForEach(func(_, p gjson.Result) bool {
|
||||||
if p.Get("thought").Bool() {
|
if p.Get("thought").Bool() {
|
||||||
@@ -583,19 +585,16 @@ func ConvertGeminiResponseToOpenAIResponsesNonStream(_ context.Context, _ string
|
|||||||
name := fc.Get("name").String()
|
name := fc.Get("name").String()
|
||||||
args := fc.Get("args")
|
args := fc.Get("args")
|
||||||
callID := fmt.Sprintf("call_%x_%d", time.Now().UnixNano(), atomic.AddUint64(&funcCallIDCounter, 1))
|
callID := fmt.Sprintf("call_%x_%d", time.Now().UnixNano(), atomic.AddUint64(&funcCallIDCounter, 1))
|
||||||
outputs = append(outputs, map[string]interface{}{
|
itemJSON := `{"id":"","type":"function_call","status":"completed","arguments":"","call_id":"","name":""}`
|
||||||
"id": fmt.Sprintf("fc_%s", callID),
|
itemJSON, _ = sjson.Set(itemJSON, "id", fmt.Sprintf("fc_%s", callID))
|
||||||
"type": "function_call",
|
itemJSON, _ = sjson.Set(itemJSON, "call_id", callID)
|
||||||
"status": "completed",
|
itemJSON, _ = sjson.Set(itemJSON, "name", name)
|
||||||
"arguments": func() string {
|
argsStr := ""
|
||||||
if args.Exists() {
|
if args.Exists() {
|
||||||
return args.Raw
|
argsStr = args.Raw
|
||||||
}
|
}
|
||||||
return ""
|
itemJSON, _ = sjson.Set(itemJSON, "arguments", argsStr)
|
||||||
}(),
|
appendOutput(itemJSON)
|
||||||
"call_id": callID,
|
|
||||||
"name": name,
|
|
||||||
})
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
@@ -605,42 +604,24 @@ func ConvertGeminiResponseToOpenAIResponsesNonStream(_ context.Context, _ string
|
|||||||
// Reasoning output item
|
// Reasoning output item
|
||||||
if reasoningText.Len() > 0 || reasoningEncrypted != "" {
|
if reasoningText.Len() > 0 || reasoningEncrypted != "" {
|
||||||
rid := strings.TrimPrefix(id, "resp_")
|
rid := strings.TrimPrefix(id, "resp_")
|
||||||
item := map[string]interface{}{
|
itemJSON := `{"id":"","type":"reasoning","encrypted_content":""}`
|
||||||
"id": fmt.Sprintf("rs_%s", rid),
|
itemJSON, _ = sjson.Set(itemJSON, "id", fmt.Sprintf("rs_%s", rid))
|
||||||
"type": "reasoning",
|
itemJSON, _ = sjson.Set(itemJSON, "encrypted_content", reasoningEncrypted)
|
||||||
"encrypted_content": reasoningEncrypted,
|
|
||||||
}
|
|
||||||
var summaries []interface{}
|
|
||||||
if reasoningText.Len() > 0 {
|
if reasoningText.Len() > 0 {
|
||||||
summaries = append(summaries, map[string]interface{}{
|
summaryJSON := `{"type":"summary_text","text":""}`
|
||||||
"type": "summary_text",
|
summaryJSON, _ = sjson.Set(summaryJSON, "text", reasoningText.String())
|
||||||
"text": reasoningText.String(),
|
itemJSON, _ = sjson.SetRaw(itemJSON, "summary", "[]")
|
||||||
})
|
itemJSON, _ = sjson.SetRaw(itemJSON, "summary.-1", summaryJSON)
|
||||||
}
|
}
|
||||||
if summaries != nil {
|
appendOutput(itemJSON)
|
||||||
item["summary"] = summaries
|
|
||||||
}
|
|
||||||
outputs = append(outputs, item)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Assistant message output item
|
// Assistant message output item
|
||||||
if haveMessage {
|
if haveMessage {
|
||||||
outputs = append(outputs, map[string]interface{}{
|
itemJSON := `{"id":"","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":""}],"role":"assistant"}`
|
||||||
"id": fmt.Sprintf("msg_%s_0", strings.TrimPrefix(id, "resp_")),
|
itemJSON, _ = sjson.Set(itemJSON, "id", fmt.Sprintf("msg_%s_0", strings.TrimPrefix(id, "resp_")))
|
||||||
"type": "message",
|
itemJSON, _ = sjson.Set(itemJSON, "content.0.text", messageText.String())
|
||||||
"status": "completed",
|
appendOutput(itemJSON)
|
||||||
"content": []interface{}{map[string]interface{}{
|
|
||||||
"type": "output_text",
|
|
||||||
"annotations": []interface{}{},
|
|
||||||
"logprobs": []interface{}{},
|
|
||||||
"text": messageText.String(),
|
|
||||||
}},
|
|
||||||
"role": "assistant",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(outputs) > 0 {
|
|
||||||
resp, _ = sjson.Set(resp, "output", outputs)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// usage mapping
|
// usage mapping
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ package claude
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
|
||||||
@@ -138,11 +137,7 @@ func ConvertClaudeRequestToOpenAI(modelName string, inputRawJSON []byte, stream
|
|||||||
|
|
||||||
// Convert input to arguments JSON string
|
// Convert input to arguments JSON string
|
||||||
if input := part.Get("input"); input.Exists() {
|
if input := part.Get("input"); input.Exists() {
|
||||||
if inputJSON, err := json.Marshal(input.Value()); err == nil {
|
toolCallJSON, _ = sjson.Set(toolCallJSON, "function.arguments", input.Raw)
|
||||||
toolCallJSON, _ = sjson.Set(toolCallJSON, "function.arguments", string(inputJSON))
|
|
||||||
} else {
|
|
||||||
toolCallJSON, _ = sjson.Set(toolCallJSON, "function.arguments", "{}")
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
toolCallJSON, _ = sjson.Set(toolCallJSON, "function.arguments", "{}")
|
toolCallJSON, _ = sjson.Set(toolCallJSON, "function.arguments", "{}")
|
||||||
}
|
}
|
||||||
@@ -191,8 +186,7 @@ func ConvertClaudeRequestToOpenAI(modelName string, inputRawJSON []byte, stream
|
|||||||
// Emit tool calls in a separate assistant message
|
// Emit tool calls in a separate assistant message
|
||||||
if role == "assistant" && len(toolCalls) > 0 {
|
if role == "assistant" && len(toolCalls) > 0 {
|
||||||
toolCallMsgJSON := `{"role":"assistant","tool_calls":[]}`
|
toolCallMsgJSON := `{"role":"assistant","tool_calls":[]}`
|
||||||
toolCallsJSON, _ := json.Marshal(toolCalls)
|
toolCallMsgJSON, _ = sjson.Set(toolCallMsgJSON, "tool_calls", toolCalls)
|
||||||
toolCallMsgJSON, _ = sjson.SetRaw(toolCallMsgJSON, "tool_calls", string(toolCallsJSON))
|
|
||||||
messagesJSON, _ = sjson.Set(messagesJSON, "-1", gjson.Parse(toolCallMsgJSON).Value())
|
messagesJSON, _ = sjson.Set(messagesJSON, "-1", gjson.Parse(toolCallMsgJSON).Value())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ package claude
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -133,24 +132,10 @@ func convertOpenAIStreamingChunkToAnthropic(rawJSON []byte, param *ConvertOpenAI
|
|||||||
if delta := root.Get("choices.0.delta"); delta.Exists() {
|
if delta := root.Get("choices.0.delta"); delta.Exists() {
|
||||||
if !param.MessageStarted {
|
if !param.MessageStarted {
|
||||||
// Send message_start event
|
// Send message_start event
|
||||||
messageStart := map[string]interface{}{
|
messageStartJSON := `{"type":"message_start","message":{"id":"","type":"message","role":"assistant","model":"","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":0,"output_tokens":0}}}`
|
||||||
"type": "message_start",
|
messageStartJSON, _ = sjson.Set(messageStartJSON, "message.id", param.MessageID)
|
||||||
"message": map[string]interface{}{
|
messageStartJSON, _ = sjson.Set(messageStartJSON, "message.model", param.Model)
|
||||||
"id": param.MessageID,
|
results = append(results, "event: message_start\ndata: "+messageStartJSON+"\n\n")
|
||||||
"type": "message",
|
|
||||||
"role": "assistant",
|
|
||||||
"model": param.Model,
|
|
||||||
"content": []interface{}{},
|
|
||||||
"stop_reason": nil,
|
|
||||||
"stop_sequence": nil,
|
|
||||||
"usage": map[string]interface{}{
|
|
||||||
"input_tokens": 0,
|
|
||||||
"output_tokens": 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
messageStartJSON, _ := json.Marshal(messageStart)
|
|
||||||
results = append(results, "event: message_start\ndata: "+string(messageStartJSON)+"\n\n")
|
|
||||||
param.MessageStarted = true
|
param.MessageStarted = true
|
||||||
|
|
||||||
// Don't send content_block_start for text here - wait for actual content
|
// Don't send content_block_start for text here - wait for actual content
|
||||||
@@ -168,29 +153,16 @@ func convertOpenAIStreamingChunkToAnthropic(rawJSON []byte, param *ConvertOpenAI
|
|||||||
param.ThinkingContentBlockIndex = param.NextContentBlockIndex
|
param.ThinkingContentBlockIndex = param.NextContentBlockIndex
|
||||||
param.NextContentBlockIndex++
|
param.NextContentBlockIndex++
|
||||||
}
|
}
|
||||||
contentBlockStart := map[string]interface{}{
|
contentBlockStartJSON := `{"type":"content_block_start","index":0,"content_block":{"type":"thinking","thinking":""}}`
|
||||||
"type": "content_block_start",
|
contentBlockStartJSON, _ = sjson.Set(contentBlockStartJSON, "index", param.ThinkingContentBlockIndex)
|
||||||
"index": param.ThinkingContentBlockIndex,
|
results = append(results, "event: content_block_start\ndata: "+contentBlockStartJSON+"\n\n")
|
||||||
"content_block": map[string]interface{}{
|
|
||||||
"type": "thinking",
|
|
||||||
"thinking": "",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
contentBlockStartJSON, _ := json.Marshal(contentBlockStart)
|
|
||||||
results = append(results, "event: content_block_start\ndata: "+string(contentBlockStartJSON)+"\n\n")
|
|
||||||
param.ThinkingContentBlockStarted = true
|
param.ThinkingContentBlockStarted = true
|
||||||
}
|
}
|
||||||
|
|
||||||
thinkingDelta := map[string]interface{}{
|
thinkingDeltaJSON := `{"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":""}}`
|
||||||
"type": "content_block_delta",
|
thinkingDeltaJSON, _ = sjson.Set(thinkingDeltaJSON, "index", param.ThinkingContentBlockIndex)
|
||||||
"index": param.ThinkingContentBlockIndex,
|
thinkingDeltaJSON, _ = sjson.Set(thinkingDeltaJSON, "delta.thinking", reasoningText)
|
||||||
"delta": map[string]interface{}{
|
results = append(results, "event: content_block_delta\ndata: "+thinkingDeltaJSON+"\n\n")
|
||||||
"type": "thinking_delta",
|
|
||||||
"thinking": reasoningText,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
thinkingDeltaJSON, _ := json.Marshal(thinkingDelta)
|
|
||||||
results = append(results, "event: content_block_delta\ndata: "+string(thinkingDeltaJSON)+"\n\n")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,29 +175,16 @@ func convertOpenAIStreamingChunkToAnthropic(rawJSON []byte, param *ConvertOpenAI
|
|||||||
param.TextContentBlockIndex = param.NextContentBlockIndex
|
param.TextContentBlockIndex = param.NextContentBlockIndex
|
||||||
param.NextContentBlockIndex++
|
param.NextContentBlockIndex++
|
||||||
}
|
}
|
||||||
contentBlockStart := map[string]interface{}{
|
contentBlockStartJSON := `{"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}`
|
||||||
"type": "content_block_start",
|
contentBlockStartJSON, _ = sjson.Set(contentBlockStartJSON, "index", param.TextContentBlockIndex)
|
||||||
"index": param.TextContentBlockIndex,
|
results = append(results, "event: content_block_start\ndata: "+contentBlockStartJSON+"\n\n")
|
||||||
"content_block": map[string]interface{}{
|
|
||||||
"type": "text",
|
|
||||||
"text": "",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
contentBlockStartJSON, _ := json.Marshal(contentBlockStart)
|
|
||||||
results = append(results, "event: content_block_start\ndata: "+string(contentBlockStartJSON)+"\n\n")
|
|
||||||
param.TextContentBlockStarted = true
|
param.TextContentBlockStarted = true
|
||||||
}
|
}
|
||||||
|
|
||||||
contentDelta := map[string]interface{}{
|
contentDeltaJSON := `{"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":""}}`
|
||||||
"type": "content_block_delta",
|
contentDeltaJSON, _ = sjson.Set(contentDeltaJSON, "index", param.TextContentBlockIndex)
|
||||||
"index": param.TextContentBlockIndex,
|
contentDeltaJSON, _ = sjson.Set(contentDeltaJSON, "delta.text", content.String())
|
||||||
"delta": map[string]interface{}{
|
results = append(results, "event: content_block_delta\ndata: "+contentDeltaJSON+"\n\n")
|
||||||
"type": "text_delta",
|
|
||||||
"text": content.String(),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
contentDeltaJSON, _ := json.Marshal(contentDelta)
|
|
||||||
results = append(results, "event: content_block_delta\ndata: "+string(contentDeltaJSON)+"\n\n")
|
|
||||||
|
|
||||||
// Accumulate content
|
// Accumulate content
|
||||||
param.ContentAccumulator.WriteString(content.String())
|
param.ContentAccumulator.WriteString(content.String())
|
||||||
@@ -263,18 +222,11 @@ func convertOpenAIStreamingChunkToAnthropic(rawJSON []byte, param *ConvertOpenAI
|
|||||||
stopTextContentBlock(param, &results)
|
stopTextContentBlock(param, &results)
|
||||||
|
|
||||||
// Send content_block_start for tool_use
|
// Send content_block_start for tool_use
|
||||||
contentBlockStart := map[string]interface{}{
|
contentBlockStartJSON := `{"type":"content_block_start","index":0,"content_block":{"type":"tool_use","id":"","name":"","input":{}}}`
|
||||||
"type": "content_block_start",
|
contentBlockStartJSON, _ = sjson.Set(contentBlockStartJSON, "index", blockIndex)
|
||||||
"index": blockIndex,
|
contentBlockStartJSON, _ = sjson.Set(contentBlockStartJSON, "content_block.id", accumulator.ID)
|
||||||
"content_block": map[string]interface{}{
|
contentBlockStartJSON, _ = sjson.Set(contentBlockStartJSON, "content_block.name", accumulator.Name)
|
||||||
"type": "tool_use",
|
results = append(results, "event: content_block_start\ndata: "+contentBlockStartJSON+"\n\n")
|
||||||
"id": accumulator.ID,
|
|
||||||
"name": accumulator.Name,
|
|
||||||
"input": map[string]interface{}{},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
contentBlockStartJSON, _ := json.Marshal(contentBlockStart)
|
|
||||||
results = append(results, "event: content_block_start\ndata: "+string(contentBlockStartJSON)+"\n\n")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle function arguments
|
// Handle function arguments
|
||||||
@@ -298,12 +250,9 @@ func convertOpenAIStreamingChunkToAnthropic(rawJSON []byte, param *ConvertOpenAI
|
|||||||
|
|
||||||
// Send content_block_stop for thinking content if needed
|
// Send content_block_stop for thinking content if needed
|
||||||
if param.ThinkingContentBlockStarted {
|
if param.ThinkingContentBlockStarted {
|
||||||
contentBlockStop := map[string]interface{}{
|
contentBlockStopJSON := `{"type":"content_block_stop","index":0}`
|
||||||
"type": "content_block_stop",
|
contentBlockStopJSON, _ = sjson.Set(contentBlockStopJSON, "index", param.ThinkingContentBlockIndex)
|
||||||
"index": param.ThinkingContentBlockIndex,
|
results = append(results, "event: content_block_stop\ndata: "+contentBlockStopJSON+"\n\n")
|
||||||
}
|
|
||||||
contentBlockStopJSON, _ := json.Marshal(contentBlockStop)
|
|
||||||
results = append(results, "event: content_block_stop\ndata: "+string(contentBlockStopJSON)+"\n\n")
|
|
||||||
param.ThinkingContentBlockStarted = false
|
param.ThinkingContentBlockStarted = false
|
||||||
param.ThinkingContentBlockIndex = -1
|
param.ThinkingContentBlockIndex = -1
|
||||||
}
|
}
|
||||||
@@ -319,24 +268,15 @@ func convertOpenAIStreamingChunkToAnthropic(rawJSON []byte, param *ConvertOpenAI
|
|||||||
|
|
||||||
// Send complete input_json_delta with all accumulated arguments
|
// Send complete input_json_delta with all accumulated arguments
|
||||||
if accumulator.Arguments.Len() > 0 {
|
if accumulator.Arguments.Len() > 0 {
|
||||||
inputDelta := map[string]interface{}{
|
inputDeltaJSON := `{"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":""}}`
|
||||||
"type": "content_block_delta",
|
inputDeltaJSON, _ = sjson.Set(inputDeltaJSON, "index", blockIndex)
|
||||||
"index": blockIndex,
|
inputDeltaJSON, _ = sjson.Set(inputDeltaJSON, "delta.partial_json", util.FixJSON(accumulator.Arguments.String()))
|
||||||
"delta": map[string]interface{}{
|
results = append(results, "event: content_block_delta\ndata: "+inputDeltaJSON+"\n\n")
|
||||||
"type": "input_json_delta",
|
|
||||||
"partial_json": util.FixJSON(accumulator.Arguments.String()),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
inputDeltaJSON, _ := json.Marshal(inputDelta)
|
|
||||||
results = append(results, "event: content_block_delta\ndata: "+string(inputDeltaJSON)+"\n\n")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
contentBlockStop := map[string]interface{}{
|
contentBlockStopJSON := `{"type":"content_block_stop","index":0}`
|
||||||
"type": "content_block_stop",
|
contentBlockStopJSON, _ = sjson.Set(contentBlockStopJSON, "index", blockIndex)
|
||||||
"index": blockIndex,
|
results = append(results, "event: content_block_stop\ndata: "+contentBlockStopJSON+"\n\n")
|
||||||
}
|
|
||||||
contentBlockStopJSON, _ := json.Marshal(contentBlockStop)
|
|
||||||
results = append(results, "event: content_block_stop\ndata: "+string(contentBlockStopJSON)+"\n\n")
|
|
||||||
delete(param.ToolCallBlockIndexes, index)
|
delete(param.ToolCallBlockIndexes, index)
|
||||||
}
|
}
|
||||||
param.ContentBlocksStopped = true
|
param.ContentBlocksStopped = true
|
||||||
@@ -361,20 +301,11 @@ func convertOpenAIStreamingChunkToAnthropic(rawJSON []byte, param *ConvertOpenAI
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Send message_delta with usage
|
// Send message_delta with usage
|
||||||
messageDelta := map[string]interface{}{
|
messageDeltaJSON := `{"type":"message_delta","delta":{"stop_reason":"","stop_sequence":null},"usage":{"input_tokens":0,"output_tokens":0}}`
|
||||||
"type": "message_delta",
|
messageDeltaJSON, _ = sjson.Set(messageDeltaJSON, "delta.stop_reason", mapOpenAIFinishReasonToAnthropic(param.FinishReason))
|
||||||
"delta": map[string]interface{}{
|
messageDeltaJSON, _ = sjson.Set(messageDeltaJSON, "usage.input_tokens", inputTokens)
|
||||||
"stop_reason": mapOpenAIFinishReasonToAnthropic(param.FinishReason),
|
messageDeltaJSON, _ = sjson.Set(messageDeltaJSON, "usage.output_tokens", outputTokens)
|
||||||
"stop_sequence": nil,
|
results = append(results, "event: message_delta\ndata: "+messageDeltaJSON+"\n\n")
|
||||||
},
|
|
||||||
"usage": map[string]interface{}{
|
|
||||||
"input_tokens": inputTokens,
|
|
||||||
"output_tokens": outputTokens,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
messageDeltaJSON, _ := json.Marshal(messageDelta)
|
|
||||||
results = append(results, "event: message_delta\ndata: "+string(messageDeltaJSON)+"\n\n")
|
|
||||||
param.MessageDeltaSent = true
|
param.MessageDeltaSent = true
|
||||||
|
|
||||||
emitMessageStopIfNeeded(param, &results)
|
emitMessageStopIfNeeded(param, &results)
|
||||||
@@ -390,12 +321,9 @@ func convertOpenAIDoneToAnthropic(param *ConvertOpenAIResponseToAnthropicParams)
|
|||||||
|
|
||||||
// Ensure all content blocks are stopped before final events
|
// Ensure all content blocks are stopped before final events
|
||||||
if param.ThinkingContentBlockStarted {
|
if param.ThinkingContentBlockStarted {
|
||||||
contentBlockStop := map[string]interface{}{
|
contentBlockStopJSON := `{"type":"content_block_stop","index":0}`
|
||||||
"type": "content_block_stop",
|
contentBlockStopJSON, _ = sjson.Set(contentBlockStopJSON, "index", param.ThinkingContentBlockIndex)
|
||||||
"index": param.ThinkingContentBlockIndex,
|
results = append(results, "event: content_block_stop\ndata: "+contentBlockStopJSON+"\n\n")
|
||||||
}
|
|
||||||
contentBlockStopJSON, _ := json.Marshal(contentBlockStop)
|
|
||||||
results = append(results, "event: content_block_stop\ndata: "+string(contentBlockStopJSON)+"\n\n")
|
|
||||||
param.ThinkingContentBlockStarted = false
|
param.ThinkingContentBlockStarted = false
|
||||||
param.ThinkingContentBlockIndex = -1
|
param.ThinkingContentBlockIndex = -1
|
||||||
}
|
}
|
||||||
@@ -408,24 +336,15 @@ func convertOpenAIDoneToAnthropic(param *ConvertOpenAIResponseToAnthropicParams)
|
|||||||
blockIndex := param.toolContentBlockIndex(index)
|
blockIndex := param.toolContentBlockIndex(index)
|
||||||
|
|
||||||
if accumulator.Arguments.Len() > 0 {
|
if accumulator.Arguments.Len() > 0 {
|
||||||
inputDelta := map[string]interface{}{
|
inputDeltaJSON := `{"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":""}}`
|
||||||
"type": "content_block_delta",
|
inputDeltaJSON, _ = sjson.Set(inputDeltaJSON, "index", blockIndex)
|
||||||
"index": blockIndex,
|
inputDeltaJSON, _ = sjson.Set(inputDeltaJSON, "delta.partial_json", util.FixJSON(accumulator.Arguments.String()))
|
||||||
"delta": map[string]interface{}{
|
results = append(results, "event: content_block_delta\ndata: "+inputDeltaJSON+"\n\n")
|
||||||
"type": "input_json_delta",
|
|
||||||
"partial_json": util.FixJSON(accumulator.Arguments.String()),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
inputDeltaJSON, _ := json.Marshal(inputDelta)
|
|
||||||
results = append(results, "event: content_block_delta\ndata: "+string(inputDeltaJSON)+"\n\n")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
contentBlockStop := map[string]interface{}{
|
contentBlockStopJSON := `{"type":"content_block_stop","index":0}`
|
||||||
"type": "content_block_stop",
|
contentBlockStopJSON, _ = sjson.Set(contentBlockStopJSON, "index", blockIndex)
|
||||||
"index": blockIndex,
|
results = append(results, "event: content_block_stop\ndata: "+contentBlockStopJSON+"\n\n")
|
||||||
}
|
|
||||||
contentBlockStopJSON, _ := json.Marshal(contentBlockStop)
|
|
||||||
results = append(results, "event: content_block_stop\ndata: "+string(contentBlockStopJSON)+"\n\n")
|
|
||||||
delete(param.ToolCallBlockIndexes, index)
|
delete(param.ToolCallBlockIndexes, index)
|
||||||
}
|
}
|
||||||
param.ContentBlocksStopped = true
|
param.ContentBlocksStopped = true
|
||||||
@@ -433,16 +352,9 @@ func convertOpenAIDoneToAnthropic(param *ConvertOpenAIResponseToAnthropicParams)
|
|||||||
|
|
||||||
// If we haven't sent message_delta yet (no usage info was received), send it now
|
// If we haven't sent message_delta yet (no usage info was received), send it now
|
||||||
if param.FinishReason != "" && !param.MessageDeltaSent {
|
if param.FinishReason != "" && !param.MessageDeltaSent {
|
||||||
messageDelta := map[string]interface{}{
|
messageDeltaJSON := `{"type":"message_delta","delta":{"stop_reason":"","stop_sequence":null}}`
|
||||||
"type": "message_delta",
|
messageDeltaJSON, _ = sjson.Set(messageDeltaJSON, "delta.stop_reason", mapOpenAIFinishReasonToAnthropic(param.FinishReason))
|
||||||
"delta": map[string]interface{}{
|
results = append(results, "event: message_delta\ndata: "+messageDeltaJSON+"\n\n")
|
||||||
"stop_reason": mapOpenAIFinishReasonToAnthropic(param.FinishReason),
|
|
||||||
"stop_sequence": nil,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
messageDeltaJSON, _ := json.Marshal(messageDelta)
|
|
||||||
results = append(results, "event: message_delta\ndata: "+string(messageDeltaJSON)+"\n\n")
|
|
||||||
param.MessageDeltaSent = true
|
param.MessageDeltaSent = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -455,105 +367,73 @@ func convertOpenAIDoneToAnthropic(param *ConvertOpenAIResponseToAnthropicParams)
|
|||||||
func convertOpenAINonStreamingToAnthropic(rawJSON []byte) []string {
|
func convertOpenAINonStreamingToAnthropic(rawJSON []byte) []string {
|
||||||
root := gjson.ParseBytes(rawJSON)
|
root := gjson.ParseBytes(rawJSON)
|
||||||
|
|
||||||
// Build Anthropic response
|
out := `{"id":"","type":"message","role":"assistant","model":"","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":0,"output_tokens":0}}`
|
||||||
response := map[string]interface{}{
|
out, _ = sjson.Set(out, "id", root.Get("id").String())
|
||||||
"id": root.Get("id").String(),
|
out, _ = sjson.Set(out, "model", root.Get("model").String())
|
||||||
"type": "message",
|
|
||||||
"role": "assistant",
|
|
||||||
"model": root.Get("model").String(),
|
|
||||||
"content": []interface{}{},
|
|
||||||
"stop_reason": nil,
|
|
||||||
"stop_sequence": nil,
|
|
||||||
"usage": map[string]interface{}{
|
|
||||||
"input_tokens": 0,
|
|
||||||
"output_tokens": 0,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process message content and tool calls
|
// Process message content and tool calls
|
||||||
var contentBlocks []interface{}
|
if choices := root.Get("choices"); choices.Exists() && choices.IsArray() && len(choices.Array()) > 0 {
|
||||||
|
|
||||||
if choices := root.Get("choices"); choices.Exists() && choices.IsArray() {
|
|
||||||
choice := choices.Array()[0] // Take first choice
|
choice := choices.Array()[0] // Take first choice
|
||||||
reasoningNode := choice.Get("message.reasoning_content")
|
|
||||||
allReasoning := collectOpenAIReasoningTexts(reasoningNode)
|
|
||||||
|
|
||||||
for _, reasoningText := range allReasoning {
|
reasoningNode := choice.Get("message.reasoning_content")
|
||||||
|
for _, reasoningText := range collectOpenAIReasoningTexts(reasoningNode) {
|
||||||
if reasoningText == "" {
|
if reasoningText == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
contentBlocks = append(contentBlocks, map[string]interface{}{
|
block := `{"type":"thinking","thinking":""}`
|
||||||
"type": "thinking",
|
block, _ = sjson.Set(block, "thinking", reasoningText)
|
||||||
"thinking": reasoningText,
|
out, _ = sjson.SetRaw(out, "content.-1", block)
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle text content
|
// Handle text content
|
||||||
if content := choice.Get("message.content"); content.Exists() && content.String() != "" {
|
if content := choice.Get("message.content"); content.Exists() && content.String() != "" {
|
||||||
textBlock := map[string]interface{}{
|
block := `{"type":"text","text":""}`
|
||||||
"type": "text",
|
block, _ = sjson.Set(block, "text", content.String())
|
||||||
"text": content.String(),
|
out, _ = sjson.SetRaw(out, "content.-1", block)
|
||||||
}
|
|
||||||
contentBlocks = append(contentBlocks, textBlock)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle tool calls
|
// Handle tool calls
|
||||||
if toolCalls := choice.Get("message.tool_calls"); toolCalls.Exists() && toolCalls.IsArray() {
|
if toolCalls := choice.Get("message.tool_calls"); toolCalls.Exists() && toolCalls.IsArray() {
|
||||||
toolCalls.ForEach(func(_, toolCall gjson.Result) bool {
|
toolCalls.ForEach(func(_, toolCall gjson.Result) bool {
|
||||||
toolUseBlock := map[string]interface{}{
|
toolUseBlock := `{"type":"tool_use","id":"","name":"","input":{}}`
|
||||||
"type": "tool_use",
|
toolUseBlock, _ = sjson.Set(toolUseBlock, "id", toolCall.Get("id").String())
|
||||||
"id": toolCall.Get("id").String(),
|
toolUseBlock, _ = sjson.Set(toolUseBlock, "name", toolCall.Get("function.name").String())
|
||||||
"name": toolCall.Get("function.name").String(),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse arguments
|
argsStr := util.FixJSON(toolCall.Get("function.arguments").String())
|
||||||
argsStr := toolCall.Get("function.arguments").String()
|
if argsStr != "" && gjson.Valid(argsStr) {
|
||||||
argsStr = util.FixJSON(argsStr)
|
argsJSON := gjson.Parse(argsStr)
|
||||||
if argsStr != "" {
|
if argsJSON.IsObject() {
|
||||||
var args interface{}
|
toolUseBlock, _ = sjson.SetRaw(toolUseBlock, "input", argsJSON.Raw)
|
||||||
if err := json.Unmarshal([]byte(argsStr), &args); err == nil {
|
|
||||||
toolUseBlock["input"] = args
|
|
||||||
} else {
|
} else {
|
||||||
toolUseBlock["input"] = map[string]interface{}{}
|
toolUseBlock, _ = sjson.SetRaw(toolUseBlock, "input", "{}")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
toolUseBlock["input"] = map[string]interface{}{}
|
toolUseBlock, _ = sjson.SetRaw(toolUseBlock, "input", "{}")
|
||||||
}
|
}
|
||||||
|
|
||||||
contentBlocks = append(contentBlocks, toolUseBlock)
|
out, _ = sjson.SetRaw(out, "content.-1", toolUseBlock)
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set stop reason
|
// Set stop reason
|
||||||
if finishReason := choice.Get("finish_reason"); finishReason.Exists() {
|
if finishReason := choice.Get("finish_reason"); finishReason.Exists() {
|
||||||
response["stop_reason"] = mapOpenAIFinishReasonToAnthropic(finishReason.String())
|
out, _ = sjson.Set(out, "stop_reason", mapOpenAIFinishReasonToAnthropic(finishReason.String()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
response["content"] = contentBlocks
|
|
||||||
|
|
||||||
// Set usage information
|
// Set usage information
|
||||||
if usage := root.Get("usage"); usage.Exists() {
|
if usage := root.Get("usage"); usage.Exists() {
|
||||||
response["usage"] = map[string]interface{}{
|
out, _ = sjson.Set(out, "usage.input_tokens", usage.Get("prompt_tokens").Int())
|
||||||
"input_tokens": usage.Get("prompt_tokens").Int(),
|
out, _ = sjson.Set(out, "usage.output_tokens", usage.Get("completion_tokens").Int())
|
||||||
"output_tokens": usage.Get("completion_tokens").Int(),
|
reasoningTokens := int64(0)
|
||||||
"reasoning_tokens": func() int64 {
|
|
||||||
if v := usage.Get("completion_tokens_details.reasoning_tokens"); v.Exists() {
|
if v := usage.Get("completion_tokens_details.reasoning_tokens"); v.Exists() {
|
||||||
return v.Int()
|
reasoningTokens = v.Int()
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}(),
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
response["usage"] = map[string]interface{}{
|
|
||||||
"input_tokens": 0,
|
|
||||||
"output_tokens": 0,
|
|
||||||
}
|
}
|
||||||
|
out, _ = sjson.Set(out, "usage.reasoning_tokens", reasoningTokens)
|
||||||
}
|
}
|
||||||
|
|
||||||
responseJSON, _ := json.Marshal(response)
|
return []string{out}
|
||||||
return []string{string(responseJSON)}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// mapOpenAIFinishReasonToAnthropic maps OpenAI finish reasons to Anthropic equivalents
|
// mapOpenAIFinishReasonToAnthropic maps OpenAI finish reasons to Anthropic equivalents
|
||||||
@@ -620,12 +500,9 @@ func stopThinkingContentBlock(param *ConvertOpenAIResponseToAnthropicParams, res
|
|||||||
if !param.ThinkingContentBlockStarted {
|
if !param.ThinkingContentBlockStarted {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
contentBlockStop := map[string]interface{}{
|
contentBlockStopJSON := `{"type":"content_block_stop","index":0}`
|
||||||
"type": "content_block_stop",
|
contentBlockStopJSON, _ = sjson.Set(contentBlockStopJSON, "index", param.ThinkingContentBlockIndex)
|
||||||
"index": param.ThinkingContentBlockIndex,
|
*results = append(*results, "event: content_block_stop\ndata: "+contentBlockStopJSON+"\n\n")
|
||||||
}
|
|
||||||
contentBlockStopJSON, _ := json.Marshal(contentBlockStop)
|
|
||||||
*results = append(*results, "event: content_block_stop\ndata: "+string(contentBlockStopJSON)+"\n\n")
|
|
||||||
param.ThinkingContentBlockStarted = false
|
param.ThinkingContentBlockStarted = false
|
||||||
param.ThinkingContentBlockIndex = -1
|
param.ThinkingContentBlockIndex = -1
|
||||||
}
|
}
|
||||||
@@ -642,12 +519,9 @@ func stopTextContentBlock(param *ConvertOpenAIResponseToAnthropicParams, results
|
|||||||
if !param.TextContentBlockStarted {
|
if !param.TextContentBlockStarted {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
contentBlockStop := map[string]interface{}{
|
contentBlockStopJSON := `{"type":"content_block_stop","index":0}`
|
||||||
"type": "content_block_stop",
|
contentBlockStopJSON, _ = sjson.Set(contentBlockStopJSON, "index", param.TextContentBlockIndex)
|
||||||
"index": param.TextContentBlockIndex,
|
*results = append(*results, "event: content_block_stop\ndata: "+contentBlockStopJSON+"\n\n")
|
||||||
}
|
|
||||||
contentBlockStopJSON, _ := json.Marshal(contentBlockStop)
|
|
||||||
*results = append(*results, "event: content_block_stop\ndata: "+string(contentBlockStopJSON)+"\n\n")
|
|
||||||
param.TextContentBlockStarted = false
|
param.TextContentBlockStarted = false
|
||||||
param.TextContentBlockIndex = -1
|
param.TextContentBlockIndex = -1
|
||||||
}
|
}
|
||||||
@@ -667,29 +541,19 @@ func ConvertOpenAIResponseToClaudeNonStream(_ context.Context, _ string, origina
|
|||||||
_ = requestRawJSON
|
_ = requestRawJSON
|
||||||
|
|
||||||
root := gjson.ParseBytes(rawJSON)
|
root := gjson.ParseBytes(rawJSON)
|
||||||
|
out := `{"id":"","type":"message","role":"assistant","model":"","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":0,"output_tokens":0}}`
|
||||||
|
out, _ = sjson.Set(out, "id", root.Get("id").String())
|
||||||
|
out, _ = sjson.Set(out, "model", root.Get("model").String())
|
||||||
|
|
||||||
response := map[string]interface{}{
|
|
||||||
"id": root.Get("id").String(),
|
|
||||||
"type": "message",
|
|
||||||
"role": "assistant",
|
|
||||||
"model": root.Get("model").String(),
|
|
||||||
"content": []interface{}{},
|
|
||||||
"stop_reason": nil,
|
|
||||||
"stop_sequence": nil,
|
|
||||||
"usage": map[string]interface{}{
|
|
||||||
"input_tokens": 0,
|
|
||||||
"output_tokens": 0,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
contentBlocks := make([]interface{}, 0)
|
|
||||||
hasToolCall := false
|
hasToolCall := false
|
||||||
|
stopReasonSet := false
|
||||||
|
|
||||||
if choices := root.Get("choices"); choices.Exists() && choices.IsArray() && len(choices.Array()) > 0 {
|
if choices := root.Get("choices"); choices.Exists() && choices.IsArray() && len(choices.Array()) > 0 {
|
||||||
choice := choices.Array()[0]
|
choice := choices.Array()[0]
|
||||||
|
|
||||||
if finishReason := choice.Get("finish_reason"); finishReason.Exists() {
|
if finishReason := choice.Get("finish_reason"); finishReason.Exists() {
|
||||||
response["stop_reason"] = mapOpenAIFinishReasonToAnthropic(finishReason.String())
|
out, _ = sjson.Set(out, "stop_reason", mapOpenAIFinishReasonToAnthropic(finishReason.String()))
|
||||||
|
stopReasonSet = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if message := choice.Get("message"); message.Exists() {
|
if message := choice.Get("message"); message.Exists() {
|
||||||
@@ -702,10 +566,9 @@ func ConvertOpenAIResponseToClaudeNonStream(_ context.Context, _ string, origina
|
|||||||
if textBuilder.Len() == 0 {
|
if textBuilder.Len() == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
contentBlocks = append(contentBlocks, map[string]interface{}{
|
block := `{"type":"text","text":""}`
|
||||||
"type": "text",
|
block, _ = sjson.Set(block, "text", textBuilder.String())
|
||||||
"text": textBuilder.String(),
|
out, _ = sjson.SetRaw(out, "content.-1", block)
|
||||||
})
|
|
||||||
textBuilder.Reset()
|
textBuilder.Reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -713,16 +576,14 @@ func ConvertOpenAIResponseToClaudeNonStream(_ context.Context, _ string, origina
|
|||||||
if thinkingBuilder.Len() == 0 {
|
if thinkingBuilder.Len() == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
contentBlocks = append(contentBlocks, map[string]interface{}{
|
block := `{"type":"thinking","thinking":""}`
|
||||||
"type": "thinking",
|
block, _ = sjson.Set(block, "thinking", thinkingBuilder.String())
|
||||||
"thinking": thinkingBuilder.String(),
|
out, _ = sjson.SetRaw(out, "content.-1", block)
|
||||||
})
|
|
||||||
thinkingBuilder.Reset()
|
thinkingBuilder.Reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, item := range contentResult.Array() {
|
for _, item := range contentResult.Array() {
|
||||||
typeStr := item.Get("type").String()
|
switch item.Get("type").String() {
|
||||||
switch typeStr {
|
|
||||||
case "text":
|
case "text":
|
||||||
flushThinking()
|
flushThinking()
|
||||||
textBuilder.WriteString(item.Get("text").String())
|
textBuilder.WriteString(item.Get("text").String())
|
||||||
@@ -733,25 +594,23 @@ func ConvertOpenAIResponseToClaudeNonStream(_ context.Context, _ string, origina
|
|||||||
if toolCalls.IsArray() {
|
if toolCalls.IsArray() {
|
||||||
toolCalls.ForEach(func(_, tc gjson.Result) bool {
|
toolCalls.ForEach(func(_, tc gjson.Result) bool {
|
||||||
hasToolCall = true
|
hasToolCall = true
|
||||||
toolUse := map[string]interface{}{
|
toolUse := `{"type":"tool_use","id":"","name":"","input":{}}`
|
||||||
"type": "tool_use",
|
toolUse, _ = sjson.Set(toolUse, "id", tc.Get("id").String())
|
||||||
"id": tc.Get("id").String(),
|
toolUse, _ = sjson.Set(toolUse, "name", tc.Get("function.name").String())
|
||||||
"name": tc.Get("function.name").String(),
|
|
||||||
}
|
|
||||||
|
|
||||||
argsStr := util.FixJSON(tc.Get("function.arguments").String())
|
argsStr := util.FixJSON(tc.Get("function.arguments").String())
|
||||||
if argsStr != "" {
|
if argsStr != "" && gjson.Valid(argsStr) {
|
||||||
var parsed interface{}
|
argsJSON := gjson.Parse(argsStr)
|
||||||
if err := json.Unmarshal([]byte(argsStr), &parsed); err == nil {
|
if argsJSON.IsObject() {
|
||||||
toolUse["input"] = parsed
|
toolUse, _ = sjson.SetRaw(toolUse, "input", argsJSON.Raw)
|
||||||
} else {
|
} else {
|
||||||
toolUse["input"] = map[string]interface{}{}
|
toolUse, _ = sjson.SetRaw(toolUse, "input", "{}")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
toolUse["input"] = map[string]interface{}{}
|
toolUse, _ = sjson.SetRaw(toolUse, "input", "{}")
|
||||||
}
|
}
|
||||||
|
|
||||||
contentBlocks = append(contentBlocks, toolUse)
|
out, _ = sjson.SetRaw(out, "content.-1", toolUse)
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -771,10 +630,9 @@ func ConvertOpenAIResponseToClaudeNonStream(_ context.Context, _ string, origina
|
|||||||
} else if contentResult.Type == gjson.String {
|
} else if contentResult.Type == gjson.String {
|
||||||
textContent := contentResult.String()
|
textContent := contentResult.String()
|
||||||
if textContent != "" {
|
if textContent != "" {
|
||||||
contentBlocks = append(contentBlocks, map[string]interface{}{
|
block := `{"type":"text","text":""}`
|
||||||
"type": "text",
|
block, _ = sjson.Set(block, "text", textContent)
|
||||||
"text": textContent,
|
out, _ = sjson.SetRaw(out, "content.-1", block)
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -784,81 +642,52 @@ func ConvertOpenAIResponseToClaudeNonStream(_ context.Context, _ string, origina
|
|||||||
if reasoningText == "" {
|
if reasoningText == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
contentBlocks = append(contentBlocks, map[string]interface{}{
|
block := `{"type":"thinking","thinking":""}`
|
||||||
"type": "thinking",
|
block, _ = sjson.Set(block, "thinking", reasoningText)
|
||||||
"thinking": reasoningText,
|
out, _ = sjson.SetRaw(out, "content.-1", block)
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if toolCalls := message.Get("tool_calls"); toolCalls.Exists() && toolCalls.IsArray() {
|
if toolCalls := message.Get("tool_calls"); toolCalls.Exists() && toolCalls.IsArray() {
|
||||||
toolCalls.ForEach(func(_, toolCall gjson.Result) bool {
|
toolCalls.ForEach(func(_, toolCall gjson.Result) bool {
|
||||||
hasToolCall = true
|
hasToolCall = true
|
||||||
toolUseBlock := map[string]interface{}{
|
toolUseBlock := `{"type":"tool_use","id":"","name":"","input":{}}`
|
||||||
"type": "tool_use",
|
toolUseBlock, _ = sjson.Set(toolUseBlock, "id", toolCall.Get("id").String())
|
||||||
"id": toolCall.Get("id").String(),
|
toolUseBlock, _ = sjson.Set(toolUseBlock, "name", toolCall.Get("function.name").String())
|
||||||
"name": toolCall.Get("function.name").String(),
|
|
||||||
}
|
|
||||||
|
|
||||||
argsStr := toolCall.Get("function.arguments").String()
|
argsStr := util.FixJSON(toolCall.Get("function.arguments").String())
|
||||||
argsStr = util.FixJSON(argsStr)
|
if argsStr != "" && gjson.Valid(argsStr) {
|
||||||
if argsStr != "" {
|
argsJSON := gjson.Parse(argsStr)
|
||||||
var args interface{}
|
if argsJSON.IsObject() {
|
||||||
if err := json.Unmarshal([]byte(argsStr), &args); err == nil {
|
toolUseBlock, _ = sjson.SetRaw(toolUseBlock, "input", argsJSON.Raw)
|
||||||
toolUseBlock["input"] = args
|
|
||||||
} else {
|
} else {
|
||||||
toolUseBlock["input"] = map[string]interface{}{}
|
toolUseBlock, _ = sjson.SetRaw(toolUseBlock, "input", "{}")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
toolUseBlock["input"] = map[string]interface{}{}
|
toolUseBlock, _ = sjson.SetRaw(toolUseBlock, "input", "{}")
|
||||||
}
|
}
|
||||||
|
|
||||||
contentBlocks = append(contentBlocks, toolUseBlock)
|
out, _ = sjson.SetRaw(out, "content.-1", toolUseBlock)
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
response["content"] = contentBlocks
|
|
||||||
|
|
||||||
if respUsage := root.Get("usage"); respUsage.Exists() {
|
if respUsage := root.Get("usage"); respUsage.Exists() {
|
||||||
usageJSON := `{}`
|
out, _ = sjson.Set(out, "usage.input_tokens", respUsage.Get("prompt_tokens").Int())
|
||||||
usageJSON, _ = sjson.Set(usageJSON, "input_tokens", respUsage.Get("prompt_tokens").Int())
|
out, _ = sjson.Set(out, "usage.output_tokens", respUsage.Get("completion_tokens").Int())
|
||||||
usageJSON, _ = sjson.Set(usageJSON, "output_tokens", respUsage.Get("completion_tokens").Int())
|
|
||||||
parsedUsage := gjson.Parse(usageJSON).Value().(map[string]interface{})
|
|
||||||
response["usage"] = parsedUsage
|
|
||||||
} else {
|
|
||||||
response["usage"] = `{"input_tokens":0,"output_tokens":0}`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if response["stop_reason"] == nil {
|
if !stopReasonSet {
|
||||||
if hasToolCall {
|
if hasToolCall {
|
||||||
response["stop_reason"] = "tool_use"
|
out, _ = sjson.Set(out, "stop_reason", "tool_use")
|
||||||
} else {
|
} else {
|
||||||
response["stop_reason"] = "end_turn"
|
out, _ = sjson.Set(out, "stop_reason", "end_turn")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !hasToolCall {
|
return out
|
||||||
if toolBlocks := response["content"].([]interface{}); len(toolBlocks) > 0 {
|
|
||||||
for _, block := range toolBlocks {
|
|
||||||
if m, ok := block.(map[string]interface{}); ok && m["type"] == "tool_use" {
|
|
||||||
hasToolCall = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if hasToolCall {
|
|
||||||
response["stop_reason"] = "tool_use"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
responseJSON, err := json.Marshal(response)
|
|
||||||
if err != nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return string(responseJSON)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func ClaudeTokenCount(ctx context.Context, count int64) string {
|
func ClaudeTokenCount(ctx context.Context, count int64) string {
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ package gemini
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"math/big"
|
"math/big"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -94,7 +93,6 @@ func ConvertGeminiRequestToOpenAI(modelName string, inputRawJSON []byte, stream
|
|||||||
out, _ = sjson.Set(out, "stream", stream)
|
out, _ = sjson.Set(out, "stream", stream)
|
||||||
|
|
||||||
// Process contents (Gemini messages) -> OpenAI messages
|
// Process contents (Gemini messages) -> OpenAI messages
|
||||||
var openAIMessages []interface{}
|
|
||||||
var toolCallIDs []string // Track tool call IDs for matching with tool results
|
var toolCallIDs []string // Track tool call IDs for matching with tool results
|
||||||
|
|
||||||
// System instruction -> OpenAI system message
|
// System instruction -> OpenAI system message
|
||||||
@@ -105,22 +103,17 @@ func ConvertGeminiRequestToOpenAI(modelName string, inputRawJSON []byte, stream
|
|||||||
}
|
}
|
||||||
if systemInstruction.Exists() {
|
if systemInstruction.Exists() {
|
||||||
parts := systemInstruction.Get("parts")
|
parts := systemInstruction.Get("parts")
|
||||||
msg := map[string]interface{}{
|
msg := `{"role":"system","content":[]}`
|
||||||
"role": "system",
|
hasContent := false
|
||||||
"content": []interface{}{},
|
|
||||||
}
|
|
||||||
|
|
||||||
var aggregatedParts []interface{}
|
|
||||||
|
|
||||||
if parts.Exists() && parts.IsArray() {
|
if parts.Exists() && parts.IsArray() {
|
||||||
parts.ForEach(func(_, part gjson.Result) bool {
|
parts.ForEach(func(_, part gjson.Result) bool {
|
||||||
// Handle text parts
|
// Handle text parts
|
||||||
if text := part.Get("text"); text.Exists() {
|
if text := part.Get("text"); text.Exists() {
|
||||||
formattedText := text.String()
|
contentPart := `{"type":"text","text":""}`
|
||||||
aggregatedParts = append(aggregatedParts, map[string]interface{}{
|
contentPart, _ = sjson.Set(contentPart, "text", text.String())
|
||||||
"type": "text",
|
msg, _ = sjson.SetRaw(msg, "content.-1", contentPart)
|
||||||
"text": formattedText,
|
hasContent = true
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle inline data (e.g., images)
|
// Handle inline data (e.g., images)
|
||||||
@@ -132,20 +125,17 @@ func ConvertGeminiRequestToOpenAI(modelName string, inputRawJSON []byte, stream
|
|||||||
data := inlineData.Get("data").String()
|
data := inlineData.Get("data").String()
|
||||||
imageURL := fmt.Sprintf("data:%s;base64,%s", mimeType, data)
|
imageURL := fmt.Sprintf("data:%s;base64,%s", mimeType, data)
|
||||||
|
|
||||||
aggregatedParts = append(aggregatedParts, map[string]interface{}{
|
contentPart := `{"type":"image_url","image_url":{"url":""}}`
|
||||||
"type": "image_url",
|
contentPart, _ = sjson.Set(contentPart, "image_url.url", imageURL)
|
||||||
"image_url": map[string]interface{}{
|
msg, _ = sjson.SetRaw(msg, "content.-1", contentPart)
|
||||||
"url": imageURL,
|
hasContent = true
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(aggregatedParts) > 0 {
|
if hasContent {
|
||||||
msg["content"] = aggregatedParts
|
out, _ = sjson.SetRaw(out, "messages.-1", msg)
|
||||||
openAIMessages = append(openAIMessages, msg)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,16 +149,15 @@ func ConvertGeminiRequestToOpenAI(modelName string, inputRawJSON []byte, stream
|
|||||||
role = "assistant"
|
role = "assistant"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create OpenAI message
|
msg := `{"role":"","content":""}`
|
||||||
msg := map[string]interface{}{
|
msg, _ = sjson.Set(msg, "role", role)
|
||||||
"role": role,
|
|
||||||
"content": "",
|
|
||||||
}
|
|
||||||
|
|
||||||
var textBuilder strings.Builder
|
var textBuilder strings.Builder
|
||||||
var aggregatedParts []interface{}
|
contentWrapper := `{"arr":[]}`
|
||||||
|
contentPartsCount := 0
|
||||||
onlyTextContent := true
|
onlyTextContent := true
|
||||||
var toolCalls []interface{}
|
toolCallsWrapper := `{"arr":[]}`
|
||||||
|
toolCallsCount := 0
|
||||||
|
|
||||||
if parts.Exists() && parts.IsArray() {
|
if parts.Exists() && parts.IsArray() {
|
||||||
parts.ForEach(func(_, part gjson.Result) bool {
|
parts.ForEach(func(_, part gjson.Result) bool {
|
||||||
@@ -176,10 +165,10 @@ func ConvertGeminiRequestToOpenAI(modelName string, inputRawJSON []byte, stream
|
|||||||
if text := part.Get("text"); text.Exists() {
|
if text := part.Get("text"); text.Exists() {
|
||||||
formattedText := text.String()
|
formattedText := text.String()
|
||||||
textBuilder.WriteString(formattedText)
|
textBuilder.WriteString(formattedText)
|
||||||
aggregatedParts = append(aggregatedParts, map[string]interface{}{
|
contentPart := `{"type":"text","text":""}`
|
||||||
"type": "text",
|
contentPart, _ = sjson.Set(contentPart, "text", formattedText)
|
||||||
"text": formattedText,
|
contentWrapper, _ = sjson.SetRaw(contentWrapper, "arr.-1", contentPart)
|
||||||
})
|
contentPartsCount++
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle inline data (e.g., images)
|
// Handle inline data (e.g., images)
|
||||||
@@ -193,12 +182,10 @@ func ConvertGeminiRequestToOpenAI(modelName string, inputRawJSON []byte, stream
|
|||||||
data := inlineData.Get("data").String()
|
data := inlineData.Get("data").String()
|
||||||
imageURL := fmt.Sprintf("data:%s;base64,%s", mimeType, data)
|
imageURL := fmt.Sprintf("data:%s;base64,%s", mimeType, data)
|
||||||
|
|
||||||
aggregatedParts = append(aggregatedParts, map[string]interface{}{
|
contentPart := `{"type":"image_url","image_url":{"url":""}}`
|
||||||
"type": "image_url",
|
contentPart, _ = sjson.Set(contentPart, "image_url.url", imageURL)
|
||||||
"image_url": map[string]interface{}{
|
contentWrapper, _ = sjson.SetRaw(contentWrapper, "arr.-1", contentPart)
|
||||||
"url": imageURL,
|
contentPartsCount++
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle function calls (Gemini) -> tool calls (OpenAI)
|
// Handle function calls (Gemini) -> tool calls (OpenAI)
|
||||||
@@ -206,44 +193,32 @@ func ConvertGeminiRequestToOpenAI(modelName string, inputRawJSON []byte, stream
|
|||||||
toolCallID := genToolCallID()
|
toolCallID := genToolCallID()
|
||||||
toolCallIDs = append(toolCallIDs, toolCallID)
|
toolCallIDs = append(toolCallIDs, toolCallID)
|
||||||
|
|
||||||
toolCall := map[string]interface{}{
|
toolCall := `{"id":"","type":"function","function":{"name":"","arguments":""}}`
|
||||||
"id": toolCallID,
|
toolCall, _ = sjson.Set(toolCall, "id", toolCallID)
|
||||||
"type": "function",
|
toolCall, _ = sjson.Set(toolCall, "function.name", functionCall.Get("name").String())
|
||||||
"function": map[string]interface{}{
|
|
||||||
"name": functionCall.Get("name").String(),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert args to arguments JSON string
|
// Convert args to arguments JSON string
|
||||||
if args := functionCall.Get("args"); args.Exists() {
|
if args := functionCall.Get("args"); args.Exists() {
|
||||||
argsJSON, _ := json.Marshal(args.Value())
|
toolCall, _ = sjson.Set(toolCall, "function.arguments", args.Raw)
|
||||||
toolCall["function"].(map[string]interface{})["arguments"] = string(argsJSON)
|
|
||||||
} else {
|
} else {
|
||||||
toolCall["function"].(map[string]interface{})["arguments"] = "{}"
|
toolCall, _ = sjson.Set(toolCall, "function.arguments", "{}")
|
||||||
}
|
}
|
||||||
|
|
||||||
toolCalls = append(toolCalls, toolCall)
|
toolCallsWrapper, _ = sjson.SetRaw(toolCallsWrapper, "arr.-1", toolCall)
|
||||||
|
toolCallsCount++
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle function responses (Gemini) -> tool role messages (OpenAI)
|
// Handle function responses (Gemini) -> tool role messages (OpenAI)
|
||||||
if functionResponse := part.Get("functionResponse"); functionResponse.Exists() {
|
if functionResponse := part.Get("functionResponse"); functionResponse.Exists() {
|
||||||
// Create tool message for function response
|
// Create tool message for function response
|
||||||
toolMsg := map[string]interface{}{
|
toolMsg := `{"role":"tool","tool_call_id":"","content":""}`
|
||||||
"role": "tool",
|
|
||||||
"tool_call_id": "", // Will be set based on context
|
|
||||||
"content": "",
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert response.content to JSON string
|
// Convert response.content to JSON string
|
||||||
if response := functionResponse.Get("response"); response.Exists() {
|
if response := functionResponse.Get("response"); response.Exists() {
|
||||||
if content = response.Get("content"); content.Exists() {
|
if contentField := response.Get("content"); contentField.Exists() {
|
||||||
// Use the content field from the response
|
toolMsg, _ = sjson.Set(toolMsg, "content", contentField.Raw)
|
||||||
contentJSON, _ := json.Marshal(content.Value())
|
|
||||||
toolMsg["content"] = string(contentJSON)
|
|
||||||
} else {
|
} else {
|
||||||
// Fallback to entire response
|
toolMsg, _ = sjson.Set(toolMsg, "content", response.Raw)
|
||||||
responseJSON, _ := json.Marshal(response.Value())
|
|
||||||
toolMsg["content"] = string(responseJSON)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -252,13 +227,13 @@ func ConvertGeminiRequestToOpenAI(modelName string, inputRawJSON []byte, stream
|
|||||||
if len(toolCallIDs) > 0 {
|
if len(toolCallIDs) > 0 {
|
||||||
// Use the last tool call ID (simple matching by function name)
|
// Use the last tool call ID (simple matching by function name)
|
||||||
// In a real implementation, you might want more sophisticated matching
|
// In a real implementation, you might want more sophisticated matching
|
||||||
toolMsg["tool_call_id"] = toolCallIDs[len(toolCallIDs)-1]
|
toolMsg, _ = sjson.Set(toolMsg, "tool_call_id", toolCallIDs[len(toolCallIDs)-1])
|
||||||
} else {
|
} else {
|
||||||
// Generate a tool call ID if none available
|
// Generate a tool call ID if none available
|
||||||
toolMsg["tool_call_id"] = genToolCallID()
|
toolMsg, _ = sjson.Set(toolMsg, "tool_call_id", genToolCallID())
|
||||||
}
|
}
|
||||||
|
|
||||||
openAIMessages = append(openAIMessages, toolMsg)
|
out, _ = sjson.SetRaw(out, "messages.-1", toolMsg)
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
@@ -266,170 +241,46 @@ func ConvertGeminiRequestToOpenAI(modelName string, inputRawJSON []byte, stream
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Set content
|
// Set content
|
||||||
if len(aggregatedParts) > 0 {
|
if contentPartsCount > 0 {
|
||||||
if onlyTextContent {
|
if onlyTextContent {
|
||||||
msg["content"] = textBuilder.String()
|
msg, _ = sjson.Set(msg, "content", textBuilder.String())
|
||||||
} else {
|
} else {
|
||||||
msg["content"] = aggregatedParts
|
msg, _ = sjson.SetRaw(msg, "content", gjson.Get(contentWrapper, "arr").Raw)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set tool calls if any
|
// Set tool calls if any
|
||||||
if len(toolCalls) > 0 {
|
if toolCallsCount > 0 {
|
||||||
msg["tool_calls"] = toolCalls
|
msg, _ = sjson.SetRaw(msg, "tool_calls", gjson.Get(toolCallsWrapper, "arr").Raw)
|
||||||
}
|
}
|
||||||
|
|
||||||
openAIMessages = append(openAIMessages, msg)
|
out, _ = sjson.SetRaw(out, "messages.-1", msg)
|
||||||
|
|
||||||
// switch role {
|
|
||||||
// case "user", "model":
|
|
||||||
// // Convert role: model -> assistant
|
|
||||||
// if role == "model" {
|
|
||||||
// role = "assistant"
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// // Create OpenAI message
|
|
||||||
// msg := map[string]interface{}{
|
|
||||||
// "role": role,
|
|
||||||
// "content": "",
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// var contentParts []string
|
|
||||||
// var toolCalls []interface{}
|
|
||||||
//
|
|
||||||
// if parts.Exists() && parts.IsArray() {
|
|
||||||
// parts.ForEach(func(_, part gjson.Result) bool {
|
|
||||||
// // Handle text parts
|
|
||||||
// if text := part.Get("text"); text.Exists() {
|
|
||||||
// contentParts = append(contentParts, text.String())
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// // Handle function calls (Gemini) -> tool calls (OpenAI)
|
|
||||||
// if functionCall := part.Get("functionCall"); functionCall.Exists() {
|
|
||||||
// toolCallID := genToolCallID()
|
|
||||||
// toolCallIDs = append(toolCallIDs, toolCallID)
|
|
||||||
//
|
|
||||||
// toolCall := map[string]interface{}{
|
|
||||||
// "id": toolCallID,
|
|
||||||
// "type": "function",
|
|
||||||
// "function": map[string]interface{}{
|
|
||||||
// "name": functionCall.Get("name").String(),
|
|
||||||
// },
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// // Convert args to arguments JSON string
|
|
||||||
// if args := functionCall.Get("args"); args.Exists() {
|
|
||||||
// argsJSON, _ := json.Marshal(args.Value())
|
|
||||||
// toolCall["function"].(map[string]interface{})["arguments"] = string(argsJSON)
|
|
||||||
// } else {
|
|
||||||
// toolCall["function"].(map[string]interface{})["arguments"] = "{}"
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// toolCalls = append(toolCalls, toolCall)
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// return true
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// // Set content
|
|
||||||
// if len(contentParts) > 0 {
|
|
||||||
// msg["content"] = strings.Join(contentParts, "")
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// // Set tool calls if any
|
|
||||||
// if len(toolCalls) > 0 {
|
|
||||||
// msg["tool_calls"] = toolCalls
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// openAIMessages = append(openAIMessages, msg)
|
|
||||||
//
|
|
||||||
// case "function":
|
|
||||||
// // Handle Gemini function role -> OpenAI tool role
|
|
||||||
// if parts.Exists() && parts.IsArray() {
|
|
||||||
// parts.ForEach(func(_, part gjson.Result) bool {
|
|
||||||
// // Handle function responses (Gemini) -> tool role messages (OpenAI)
|
|
||||||
// if functionResponse := part.Get("functionResponse"); functionResponse.Exists() {
|
|
||||||
// // Create tool message for function response
|
|
||||||
// toolMsg := map[string]interface{}{
|
|
||||||
// "role": "tool",
|
|
||||||
// "tool_call_id": "", // Will be set based on context
|
|
||||||
// "content": "",
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// // Convert response.content to JSON string
|
|
||||||
// if response := functionResponse.Get("response"); response.Exists() {
|
|
||||||
// if content = response.Get("content"); content.Exists() {
|
|
||||||
// // Use the content field from the response
|
|
||||||
// contentJSON, _ := json.Marshal(content.Value())
|
|
||||||
// toolMsg["content"] = string(contentJSON)
|
|
||||||
// } else {
|
|
||||||
// // Fallback to entire response
|
|
||||||
// responseJSON, _ := json.Marshal(response.Value())
|
|
||||||
// toolMsg["content"] = string(responseJSON)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// // Try to match with previous tool call ID
|
|
||||||
// _ = functionResponse.Get("name").String() // functionName not used for now
|
|
||||||
// if len(toolCallIDs) > 0 {
|
|
||||||
// // Use the last tool call ID (simple matching by function name)
|
|
||||||
// // In a real implementation, you might want more sophisticated matching
|
|
||||||
// toolMsg["tool_call_id"] = toolCallIDs[len(toolCallIDs)-1]
|
|
||||||
// } else {
|
|
||||||
// // Generate a tool call ID if none available
|
|
||||||
// toolMsg["tool_call_id"] = genToolCallID()
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// openAIMessages = append(openAIMessages, toolMsg)
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// return true
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set messages
|
|
||||||
if len(openAIMessages) > 0 {
|
|
||||||
messagesJSON, _ := json.Marshal(openAIMessages)
|
|
||||||
out, _ = sjson.SetRaw(out, "messages", string(messagesJSON))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tools mapping: Gemini tools -> OpenAI tools
|
// Tools mapping: Gemini tools -> OpenAI tools
|
||||||
if tools := root.Get("tools"); tools.Exists() && tools.IsArray() {
|
if tools := root.Get("tools"); tools.Exists() && tools.IsArray() {
|
||||||
var openAITools []interface{}
|
|
||||||
tools.ForEach(func(_, tool gjson.Result) bool {
|
tools.ForEach(func(_, tool gjson.Result) bool {
|
||||||
if functionDeclarations := tool.Get("functionDeclarations"); functionDeclarations.Exists() && functionDeclarations.IsArray() {
|
if functionDeclarations := tool.Get("functionDeclarations"); functionDeclarations.Exists() && functionDeclarations.IsArray() {
|
||||||
functionDeclarations.ForEach(func(_, funcDecl gjson.Result) bool {
|
functionDeclarations.ForEach(func(_, funcDecl gjson.Result) bool {
|
||||||
openAITool := map[string]interface{}{
|
openAITool := `{"type":"function","function":{"name":"","description":""}}`
|
||||||
"type": "function",
|
openAITool, _ = sjson.Set(openAITool, "function.name", funcDecl.Get("name").String())
|
||||||
"function": map[string]interface{}{
|
openAITool, _ = sjson.Set(openAITool, "function.description", funcDecl.Get("description").String())
|
||||||
"name": funcDecl.Get("name").String(),
|
|
||||||
"description": funcDecl.Get("description").String(),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert parameters schema
|
// Convert parameters schema
|
||||||
if parameters := funcDecl.Get("parameters"); parameters.Exists() {
|
if parameters := funcDecl.Get("parameters"); parameters.Exists() {
|
||||||
openAITool["function"].(map[string]interface{})["parameters"] = parameters.Value()
|
openAITool, _ = sjson.SetRaw(openAITool, "function.parameters", parameters.Raw)
|
||||||
} else if parameters = funcDecl.Get("parametersJsonSchema"); parameters.Exists() {
|
} else if parameters := funcDecl.Get("parametersJsonSchema"); parameters.Exists() {
|
||||||
openAITool["function"].(map[string]interface{})["parameters"] = parameters.Value()
|
openAITool, _ = sjson.SetRaw(openAITool, "function.parameters", parameters.Raw)
|
||||||
}
|
}
|
||||||
|
|
||||||
openAITools = append(openAITools, openAITool)
|
out, _ = sjson.SetRaw(out, "tools.-1", openAITool)
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
|
|
||||||
if len(openAITools) > 0 {
|
|
||||||
toolsJSON, _ := json.Marshal(openAITools)
|
|
||||||
out, _ = sjson.SetRaw(out, "tools", string(toolsJSON))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tool choice mapping (Gemini doesn't have direct equivalent, but we can handle it)
|
// Tool choice mapping (Gemini doesn't have direct equivalent, but we can handle it)
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ package gemini
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -84,15 +83,12 @@ func ConvertOpenAIResponseToGemini(_ context.Context, _ string, originalRequestR
|
|||||||
template, _ = sjson.Set(template, "model", model.String())
|
template, _ = sjson.Set(template, "model", model.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
usageObj := map[string]interface{}{
|
template, _ = sjson.Set(template, "usageMetadata.promptTokenCount", usage.Get("prompt_tokens").Int())
|
||||||
"promptTokenCount": usage.Get("prompt_tokens").Int(),
|
template, _ = sjson.Set(template, "usageMetadata.candidatesTokenCount", usage.Get("completion_tokens").Int())
|
||||||
"candidatesTokenCount": usage.Get("completion_tokens").Int(),
|
template, _ = sjson.Set(template, "usageMetadata.totalTokenCount", usage.Get("total_tokens").Int())
|
||||||
"totalTokenCount": usage.Get("total_tokens").Int(),
|
|
||||||
}
|
|
||||||
if reasoningTokens := reasoningTokensFromUsage(usage); reasoningTokens > 0 {
|
if reasoningTokens := reasoningTokensFromUsage(usage); reasoningTokens > 0 {
|
||||||
usageObj["thoughtsTokenCount"] = reasoningTokens
|
template, _ = sjson.Set(template, "usageMetadata.thoughtsTokenCount", reasoningTokens)
|
||||||
}
|
}
|
||||||
template, _ = sjson.Set(template, "usageMetadata", usageObj)
|
|
||||||
return []string{template}
|
return []string{template}
|
||||||
}
|
}
|
||||||
return []string{}
|
return []string{}
|
||||||
@@ -133,13 +129,8 @@ func ConvertOpenAIResponseToGemini(_ context.Context, _ string, originalRequestR
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
reasoningTemplate := baseTemplate
|
reasoningTemplate := baseTemplate
|
||||||
parts := []interface{}{
|
reasoningTemplate, _ = sjson.Set(reasoningTemplate, "candidates.0.content.parts.0.thought", true)
|
||||||
map[string]interface{}{
|
reasoningTemplate, _ = sjson.Set(reasoningTemplate, "candidates.0.content.parts.0.text", reasoningText)
|
||||||
"thought": true,
|
|
||||||
"text": reasoningText,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
reasoningTemplate, _ = sjson.Set(reasoningTemplate, "candidates.0.content.parts", parts)
|
|
||||||
chunkOutputs = append(chunkOutputs, reasoningTemplate)
|
chunkOutputs = append(chunkOutputs, reasoningTemplate)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -150,13 +141,8 @@ func ConvertOpenAIResponseToGemini(_ context.Context, _ string, originalRequestR
|
|||||||
(*param).(*ConvertOpenAIResponseToGeminiParams).ContentAccumulator.WriteString(contentText)
|
(*param).(*ConvertOpenAIResponseToGeminiParams).ContentAccumulator.WriteString(contentText)
|
||||||
|
|
||||||
// Create text part for this delta
|
// Create text part for this delta
|
||||||
parts := []interface{}{
|
|
||||||
map[string]interface{}{
|
|
||||||
"text": contentText,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
contentTemplate := baseTemplate
|
contentTemplate := baseTemplate
|
||||||
contentTemplate, _ = sjson.Set(contentTemplate, "candidates.0.content.parts", parts)
|
contentTemplate, _ = sjson.Set(contentTemplate, "candidates.0.content.parts.0.text", contentText)
|
||||||
chunkOutputs = append(chunkOutputs, contentTemplate)
|
chunkOutputs = append(chunkOutputs, contentTemplate)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -225,24 +211,13 @@ func ConvertOpenAIResponseToGemini(_ context.Context, _ string, originalRequestR
|
|||||||
|
|
||||||
// If we have accumulated tool calls, output them now
|
// If we have accumulated tool calls, output them now
|
||||||
if len((*param).(*ConvertOpenAIResponseToGeminiParams).ToolCallsAccumulator) > 0 {
|
if len((*param).(*ConvertOpenAIResponseToGeminiParams).ToolCallsAccumulator) > 0 {
|
||||||
var parts []interface{}
|
partIndex := 0
|
||||||
for _, accumulator := range (*param).(*ConvertOpenAIResponseToGeminiParams).ToolCallsAccumulator {
|
for _, accumulator := range (*param).(*ConvertOpenAIResponseToGeminiParams).ToolCallsAccumulator {
|
||||||
argsStr := accumulator.Arguments.String()
|
namePath := fmt.Sprintf("candidates.0.content.parts.%d.functionCall.name", partIndex)
|
||||||
var argsMap map[string]interface{}
|
argsPath := fmt.Sprintf("candidates.0.content.parts.%d.functionCall.args", partIndex)
|
||||||
|
template, _ = sjson.Set(template, namePath, accumulator.Name)
|
||||||
argsMap = parseArgsToMap(argsStr)
|
template, _ = sjson.SetRaw(template, argsPath, parseArgsToObjectRaw(accumulator.Arguments.String()))
|
||||||
|
partIndex++
|
||||||
functionCallPart := map[string]interface{}{
|
|
||||||
"functionCall": map[string]interface{}{
|
|
||||||
"name": accumulator.Name,
|
|
||||||
"args": argsMap,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
parts = append(parts, functionCallPart)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(parts) > 0 {
|
|
||||||
template, _ = sjson.Set(template, "candidates.0.content.parts", parts)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear accumulators
|
// Clear accumulators
|
||||||
@@ -255,15 +230,12 @@ func ConvertOpenAIResponseToGemini(_ context.Context, _ string, originalRequestR
|
|||||||
|
|
||||||
// Handle usage information
|
// Handle usage information
|
||||||
if usage := root.Get("usage"); usage.Exists() {
|
if usage := root.Get("usage"); usage.Exists() {
|
||||||
usageObj := map[string]interface{}{
|
template, _ = sjson.Set(template, "usageMetadata.promptTokenCount", usage.Get("prompt_tokens").Int())
|
||||||
"promptTokenCount": usage.Get("prompt_tokens").Int(),
|
template, _ = sjson.Set(template, "usageMetadata.candidatesTokenCount", usage.Get("completion_tokens").Int())
|
||||||
"candidatesTokenCount": usage.Get("completion_tokens").Int(),
|
template, _ = sjson.Set(template, "usageMetadata.totalTokenCount", usage.Get("total_tokens").Int())
|
||||||
"totalTokenCount": usage.Get("total_tokens").Int(),
|
|
||||||
}
|
|
||||||
if reasoningTokens := reasoningTokensFromUsage(usage); reasoningTokens > 0 {
|
if reasoningTokens := reasoningTokensFromUsage(usage); reasoningTokens > 0 {
|
||||||
usageObj["thoughtsTokenCount"] = reasoningTokens
|
template, _ = sjson.Set(template, "usageMetadata.thoughtsTokenCount", reasoningTokens)
|
||||||
}
|
}
|
||||||
template, _ = sjson.Set(template, "usageMetadata", usageObj)
|
|
||||||
results = append(results, template)
|
results = append(results, template)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -291,46 +263,54 @@ func mapOpenAIFinishReasonToGemini(openAIReason string) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseArgsToMap safely parses a JSON string of function arguments into a map.
|
// parseArgsToObjectRaw safely parses a JSON string of function arguments into an object JSON string.
|
||||||
// It returns an empty map if the input is empty or cannot be parsed as a JSON object.
|
// It returns "{}" if the input is empty or cannot be parsed as a JSON object.
|
||||||
func parseArgsToMap(argsStr string) map[string]interface{} {
|
func parseArgsToObjectRaw(argsStr string) string {
|
||||||
trimmed := strings.TrimSpace(argsStr)
|
trimmed := strings.TrimSpace(argsStr)
|
||||||
if trimmed == "" || trimmed == "{}" {
|
if trimmed == "" || trimmed == "{}" {
|
||||||
return map[string]interface{}{}
|
return "{}"
|
||||||
}
|
}
|
||||||
|
|
||||||
// First try strict JSON
|
// First try strict JSON
|
||||||
var out map[string]interface{}
|
if gjson.Valid(trimmed) {
|
||||||
if errUnmarshal := json.Unmarshal([]byte(trimmed), &out); errUnmarshal == nil {
|
strict := gjson.Parse(trimmed)
|
||||||
return out
|
if strict.IsObject() {
|
||||||
|
return strict.Raw
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tolerant parse: handle streams where values are barewords (e.g., 北京, celsius)
|
// Tolerant parse: handle streams where values are barewords (e.g., 北京, celsius)
|
||||||
tolerant := tolerantParseJSONMap(trimmed)
|
tolerant := tolerantParseJSONObjectRaw(trimmed)
|
||||||
if len(tolerant) > 0 {
|
if tolerant != "{}" {
|
||||||
return tolerant
|
return tolerant
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback: return empty object when parsing fails
|
// Fallback: return empty object when parsing fails
|
||||||
return map[string]interface{}{}
|
return "{}"
|
||||||
}
|
}
|
||||||
|
|
||||||
// tolerantParseJSONMap attempts to parse a JSON-like object string into a map, tolerating
|
func escapeSjsonPathKey(key string) string {
|
||||||
|
key = strings.ReplaceAll(key, `\`, `\\`)
|
||||||
|
key = strings.ReplaceAll(key, `.`, `\.`)
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
|
||||||
|
// tolerantParseJSONObjectRaw attempts to parse a JSON-like object string into a JSON object string, tolerating
|
||||||
// bareword values (unquoted strings) commonly seen during streamed tool calls.
|
// bareword values (unquoted strings) commonly seen during streamed tool calls.
|
||||||
// Example input: {"location": 北京, "unit": celsius}
|
// Example input: {"location": 北京, "unit": celsius}
|
||||||
func tolerantParseJSONMap(s string) map[string]interface{} {
|
func tolerantParseJSONObjectRaw(s string) string {
|
||||||
// Ensure we operate within the outermost braces if present
|
// Ensure we operate within the outermost braces if present
|
||||||
start := strings.Index(s, "{")
|
start := strings.Index(s, "{")
|
||||||
end := strings.LastIndex(s, "}")
|
end := strings.LastIndex(s, "}")
|
||||||
if start == -1 || end == -1 || start >= end {
|
if start == -1 || end == -1 || start >= end {
|
||||||
return map[string]interface{}{}
|
return "{}"
|
||||||
}
|
}
|
||||||
content := s[start+1 : end]
|
content := s[start+1 : end]
|
||||||
|
|
||||||
runes := []rune(content)
|
runes := []rune(content)
|
||||||
n := len(runes)
|
n := len(runes)
|
||||||
i := 0
|
i := 0
|
||||||
result := make(map[string]interface{})
|
result := "{}"
|
||||||
|
|
||||||
for i < n {
|
for i < n {
|
||||||
// Skip whitespace and commas
|
// Skip whitespace and commas
|
||||||
@@ -356,6 +336,7 @@ func tolerantParseJSONMap(s string) map[string]interface{} {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
keyName := jsonStringTokenToRawString(keyToken)
|
keyName := jsonStringTokenToRawString(keyToken)
|
||||||
|
sjsonKey := escapeSjsonPathKey(keyName)
|
||||||
i = nextIdx
|
i = nextIdx
|
||||||
|
|
||||||
// Skip whitespace
|
// Skip whitespace
|
||||||
@@ -375,17 +356,16 @@ func tolerantParseJSONMap(s string) map[string]interface{} {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Parse value (string, number, object/array, bareword)
|
// Parse value (string, number, object/array, bareword)
|
||||||
var value interface{}
|
|
||||||
switch runes[i] {
|
switch runes[i] {
|
||||||
case '"':
|
case '"':
|
||||||
// JSON string
|
// JSON string
|
||||||
valToken, ni := parseJSONStringRunes(runes, i)
|
valToken, ni := parseJSONStringRunes(runes, i)
|
||||||
if ni == -1 {
|
if ni == -1 {
|
||||||
// Malformed; treat as empty string
|
// Malformed; treat as empty string
|
||||||
value = ""
|
result, _ = sjson.Set(result, sjsonKey, "")
|
||||||
i = n
|
i = n
|
||||||
} else {
|
} else {
|
||||||
value = jsonStringTokenToRawString(valToken)
|
result, _ = sjson.Set(result, sjsonKey, jsonStringTokenToRawString(valToken))
|
||||||
i = ni
|
i = ni
|
||||||
}
|
}
|
||||||
case '{', '[':
|
case '{', '[':
|
||||||
@@ -394,11 +374,10 @@ func tolerantParseJSONMap(s string) map[string]interface{} {
|
|||||||
if ni == -1 {
|
if ni == -1 {
|
||||||
i = n
|
i = n
|
||||||
} else {
|
} else {
|
||||||
var anyVal interface{}
|
if gjson.Valid(seg) {
|
||||||
if errUnmarshal := json.Unmarshal([]byte(seg), &anyVal); errUnmarshal == nil {
|
result, _ = sjson.SetRaw(result, sjsonKey, seg)
|
||||||
value = anyVal
|
|
||||||
} else {
|
} else {
|
||||||
value = seg
|
result, _ = sjson.Set(result, sjsonKey, seg)
|
||||||
}
|
}
|
||||||
i = ni
|
i = ni
|
||||||
}
|
}
|
||||||
@@ -411,21 +390,19 @@ func tolerantParseJSONMap(s string) map[string]interface{} {
|
|||||||
token := strings.TrimSpace(string(runes[i:j]))
|
token := strings.TrimSpace(string(runes[i:j]))
|
||||||
// Interpret common JSON atoms and numbers; otherwise treat as string
|
// Interpret common JSON atoms and numbers; otherwise treat as string
|
||||||
if token == "true" {
|
if token == "true" {
|
||||||
value = true
|
result, _ = sjson.Set(result, sjsonKey, true)
|
||||||
} else if token == "false" {
|
} else if token == "false" {
|
||||||
value = false
|
result, _ = sjson.Set(result, sjsonKey, false)
|
||||||
} else if token == "null" {
|
} else if token == "null" {
|
||||||
value = nil
|
result, _ = sjson.Set(result, sjsonKey, nil)
|
||||||
} else if numVal, ok := tryParseNumber(token); ok {
|
} else if numVal, ok := tryParseNumber(token); ok {
|
||||||
value = numVal
|
result, _ = sjson.Set(result, sjsonKey, numVal)
|
||||||
} else {
|
} else {
|
||||||
value = token
|
result, _ = sjson.Set(result, sjsonKey, token)
|
||||||
}
|
}
|
||||||
i = j
|
i = j
|
||||||
}
|
}
|
||||||
|
|
||||||
result[keyName] = value
|
|
||||||
|
|
||||||
// Skip trailing whitespace and optional comma before next pair
|
// Skip trailing whitespace and optional comma before next pair
|
||||||
for i < n && (runes[i] == ' ' || runes[i] == '\n' || runes[i] == '\r' || runes[i] == '\t') {
|
for i < n && (runes[i] == ' ' || runes[i] == '\n' || runes[i] == '\r' || runes[i] == '\t') {
|
||||||
i++
|
i++
|
||||||
@@ -463,9 +440,9 @@ func parseJSONStringRunes(runes []rune, start int) (string, int) {
|
|||||||
|
|
||||||
// jsonStringTokenToRawString converts a JSON string token (including quotes) to a raw Go string value.
|
// jsonStringTokenToRawString converts a JSON string token (including quotes) to a raw Go string value.
|
||||||
func jsonStringTokenToRawString(token string) string {
|
func jsonStringTokenToRawString(token string) string {
|
||||||
var s string
|
r := gjson.Parse(token)
|
||||||
if errUnmarshal := json.Unmarshal([]byte(token), &s); errUnmarshal == nil {
|
if r.Type == gjson.String {
|
||||||
return s
|
return r.String()
|
||||||
}
|
}
|
||||||
// Fallback: strip surrounding quotes if present
|
// Fallback: strip surrounding quotes if present
|
||||||
if len(token) >= 2 && token[0] == '"' && token[len(token)-1] == '"' {
|
if len(token) >= 2 && token[0] == '"' && token[len(token)-1] == '"' {
|
||||||
@@ -579,7 +556,7 @@ func ConvertOpenAIResponseToGeminiNonStream(_ context.Context, _ string, origina
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var parts []interface{}
|
partIndex := 0
|
||||||
|
|
||||||
// Handle reasoning content before visible text
|
// Handle reasoning content before visible text
|
||||||
if reasoning := message.Get("reasoning_content"); reasoning.Exists() {
|
if reasoning := message.Get("reasoning_content"); reasoning.Exists() {
|
||||||
@@ -587,18 +564,16 @@ func ConvertOpenAIResponseToGeminiNonStream(_ context.Context, _ string, origina
|
|||||||
if reasoningText == "" {
|
if reasoningText == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
parts = append(parts, map[string]interface{}{
|
out, _ = sjson.Set(out, fmt.Sprintf("candidates.0.content.parts.%d.thought", partIndex), true)
|
||||||
"thought": true,
|
out, _ = sjson.Set(out, fmt.Sprintf("candidates.0.content.parts.%d.text", partIndex), reasoningText)
|
||||||
"text": reasoningText,
|
partIndex++
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle content first
|
// Handle content first
|
||||||
if content := message.Get("content"); content.Exists() && content.String() != "" {
|
if content := message.Get("content"); content.Exists() && content.String() != "" {
|
||||||
parts = append(parts, map[string]interface{}{
|
out, _ = sjson.Set(out, fmt.Sprintf("candidates.0.content.parts.%d.text", partIndex), content.String())
|
||||||
"text": content.String(),
|
partIndex++
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle tool calls
|
// Handle tool calls
|
||||||
@@ -609,27 +584,16 @@ func ConvertOpenAIResponseToGeminiNonStream(_ context.Context, _ string, origina
|
|||||||
functionName := function.Get("name").String()
|
functionName := function.Get("name").String()
|
||||||
functionArgs := function.Get("arguments").String()
|
functionArgs := function.Get("arguments").String()
|
||||||
|
|
||||||
// Parse arguments
|
namePath := fmt.Sprintf("candidates.0.content.parts.%d.functionCall.name", partIndex)
|
||||||
var argsMap map[string]interface{}
|
argsPath := fmt.Sprintf("candidates.0.content.parts.%d.functionCall.args", partIndex)
|
||||||
argsMap = parseArgsToMap(functionArgs)
|
out, _ = sjson.Set(out, namePath, functionName)
|
||||||
|
out, _ = sjson.SetRaw(out, argsPath, parseArgsToObjectRaw(functionArgs))
|
||||||
functionCallPart := map[string]interface{}{
|
partIndex++
|
||||||
"functionCall": map[string]interface{}{
|
|
||||||
"name": functionName,
|
|
||||||
"args": argsMap,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
parts = append(parts, functionCallPart)
|
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set parts
|
|
||||||
if len(parts) > 0 {
|
|
||||||
out, _ = sjson.Set(out, "candidates.0.content.parts", parts)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle finish reason
|
// Handle finish reason
|
||||||
if finishReason := choice.Get("finish_reason"); finishReason.Exists() {
|
if finishReason := choice.Get("finish_reason"); finishReason.Exists() {
|
||||||
geminiFinishReason := mapOpenAIFinishReasonToGemini(finishReason.String())
|
geminiFinishReason := mapOpenAIFinishReasonToGemini(finishReason.String())
|
||||||
@@ -645,15 +609,12 @@ func ConvertOpenAIResponseToGeminiNonStream(_ context.Context, _ string, origina
|
|||||||
|
|
||||||
// Handle usage information
|
// Handle usage information
|
||||||
if usage := root.Get("usage"); usage.Exists() {
|
if usage := root.Get("usage"); usage.Exists() {
|
||||||
usageObj := map[string]interface{}{
|
out, _ = sjson.Set(out, "usageMetadata.promptTokenCount", usage.Get("prompt_tokens").Int())
|
||||||
"promptTokenCount": usage.Get("prompt_tokens").Int(),
|
out, _ = sjson.Set(out, "usageMetadata.candidatesTokenCount", usage.Get("completion_tokens").Int())
|
||||||
"candidatesTokenCount": usage.Get("completion_tokens").Int(),
|
out, _ = sjson.Set(out, "usageMetadata.totalTokenCount", usage.Get("total_tokens").Int())
|
||||||
"totalTokenCount": usage.Get("total_tokens").Int(),
|
|
||||||
}
|
|
||||||
if reasoningTokens := reasoningTokensFromUsage(usage); reasoningTokens > 0 {
|
if reasoningTokens := reasoningTokensFromUsage(usage); reasoningTokens > 0 {
|
||||||
usageObj["thoughtsTokenCount"] = reasoningTokens
|
out, _ = sjson.Set(out, "usageMetadata.thoughtsTokenCount", reasoningTokens)
|
||||||
}
|
}
|
||||||
out, _ = sjson.Set(out, "usageMetadata", usageObj)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return out
|
return out
|
||||||
|
|||||||
@@ -484,16 +484,12 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx context.Context,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Build response.output using aggregated buffers
|
// Build response.output using aggregated buffers
|
||||||
var outputs []interface{}
|
outputsWrapper := `{"arr":[]}`
|
||||||
if st.ReasoningBuf.Len() > 0 {
|
if st.ReasoningBuf.Len() > 0 {
|
||||||
outputs = append(outputs, map[string]interface{}{
|
item := `{"id":"","type":"reasoning","summary":[{"type":"summary_text","text":""}]}`
|
||||||
"id": st.ReasoningID,
|
item, _ = sjson.Set(item, "id", st.ReasoningID)
|
||||||
"type": "reasoning",
|
item, _ = sjson.Set(item, "summary.0.text", st.ReasoningBuf.String())
|
||||||
"summary": []interface{}{map[string]interface{}{
|
outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", item)
|
||||||
"type": "summary_text",
|
|
||||||
"text": st.ReasoningBuf.String(),
|
|
||||||
}},
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
// Append message items in ascending index order
|
// Append message items in ascending index order
|
||||||
if len(st.MsgItemAdded) > 0 {
|
if len(st.MsgItemAdded) > 0 {
|
||||||
@@ -513,18 +509,10 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx context.Context,
|
|||||||
if b := st.MsgTextBuf[i]; b != nil {
|
if b := st.MsgTextBuf[i]; b != nil {
|
||||||
txt = b.String()
|
txt = b.String()
|
||||||
}
|
}
|
||||||
outputs = append(outputs, map[string]interface{}{
|
item := `{"id":"","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":""}],"role":"assistant"}`
|
||||||
"id": fmt.Sprintf("msg_%s_%d", st.ResponseID, i),
|
item, _ = sjson.Set(item, "id", fmt.Sprintf("msg_%s_%d", st.ResponseID, i))
|
||||||
"type": "message",
|
item, _ = sjson.Set(item, "content.0.text", txt)
|
||||||
"status": "completed",
|
outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", item)
|
||||||
"content": []interface{}{map[string]interface{}{
|
|
||||||
"type": "output_text",
|
|
||||||
"annotations": []interface{}{},
|
|
||||||
"logprobs": []interface{}{},
|
|
||||||
"text": txt,
|
|
||||||
}},
|
|
||||||
"role": "assistant",
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(st.FuncArgsBuf) > 0 {
|
if len(st.FuncArgsBuf) > 0 {
|
||||||
@@ -547,18 +535,16 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx context.Context,
|
|||||||
}
|
}
|
||||||
callID := st.FuncCallIDs[i]
|
callID := st.FuncCallIDs[i]
|
||||||
name := st.FuncNames[i]
|
name := st.FuncNames[i]
|
||||||
outputs = append(outputs, map[string]interface{}{
|
item := `{"id":"","type":"function_call","status":"completed","arguments":"","call_id":"","name":""}`
|
||||||
"id": fmt.Sprintf("fc_%s", callID),
|
item, _ = sjson.Set(item, "id", fmt.Sprintf("fc_%s", callID))
|
||||||
"type": "function_call",
|
item, _ = sjson.Set(item, "arguments", args)
|
||||||
"status": "completed",
|
item, _ = sjson.Set(item, "call_id", callID)
|
||||||
"arguments": args,
|
item, _ = sjson.Set(item, "name", name)
|
||||||
"call_id": callID,
|
outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", item)
|
||||||
"name": name,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(outputs) > 0 {
|
if gjson.Get(outputsWrapper, "arr.#").Int() > 0 {
|
||||||
completed, _ = sjson.Set(completed, "response.output", outputs)
|
completed, _ = sjson.SetRaw(completed, "response.output", gjson.Get(outputsWrapper, "arr").Raw)
|
||||||
}
|
}
|
||||||
if st.UsageSeen {
|
if st.UsageSeen {
|
||||||
completed, _ = sjson.Set(completed, "response.usage.input_tokens", st.PromptTokens)
|
completed, _ = sjson.Set(completed, "response.usage.input_tokens", st.PromptTokens)
|
||||||
@@ -681,7 +667,7 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponsesNonStream(_ context.Co
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Build output list from choices[...]
|
// Build output list from choices[...]
|
||||||
var outputs []interface{}
|
outputsWrapper := `{"arr":[]}`
|
||||||
// Detect and capture reasoning content if present
|
// Detect and capture reasoning content if present
|
||||||
rcText := gjson.GetBytes(rawJSON, "choices.0.message.reasoning_content").String()
|
rcText := gjson.GetBytes(rawJSON, "choices.0.message.reasoning_content").String()
|
||||||
includeReasoning := rcText != ""
|
includeReasoning := rcText != ""
|
||||||
@@ -693,21 +679,14 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponsesNonStream(_ context.Co
|
|||||||
if strings.HasPrefix(rid, "resp_") {
|
if strings.HasPrefix(rid, "resp_") {
|
||||||
rid = strings.TrimPrefix(rid, "resp_")
|
rid = strings.TrimPrefix(rid, "resp_")
|
||||||
}
|
}
|
||||||
reasoningItem := map[string]interface{}{
|
|
||||||
"id": fmt.Sprintf("rs_%s", rid),
|
|
||||||
"type": "reasoning",
|
|
||||||
"encrypted_content": "",
|
|
||||||
}
|
|
||||||
// Prefer summary_text from reasoning_content; encrypted_content is optional
|
// Prefer summary_text from reasoning_content; encrypted_content is optional
|
||||||
var summaries []interface{}
|
reasoningItem := `{"id":"","type":"reasoning","encrypted_content":"","summary":[]}`
|
||||||
|
reasoningItem, _ = sjson.Set(reasoningItem, "id", fmt.Sprintf("rs_%s", rid))
|
||||||
if rcText != "" {
|
if rcText != "" {
|
||||||
summaries = append(summaries, map[string]interface{}{
|
reasoningItem, _ = sjson.Set(reasoningItem, "summary.0.type", "summary_text")
|
||||||
"type": "summary_text",
|
reasoningItem, _ = sjson.Set(reasoningItem, "summary.0.text", rcText)
|
||||||
"text": rcText,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
reasoningItem["summary"] = summaries
|
outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", reasoningItem)
|
||||||
outputs = append(outputs, reasoningItem)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if choices := root.Get("choices"); choices.Exists() && choices.IsArray() {
|
if choices := root.Get("choices"); choices.Exists() && choices.IsArray() {
|
||||||
@@ -716,18 +695,10 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponsesNonStream(_ context.Co
|
|||||||
if msg.Exists() {
|
if msg.Exists() {
|
||||||
// Text message part
|
// Text message part
|
||||||
if c := msg.Get("content"); c.Exists() && c.String() != "" {
|
if c := msg.Get("content"); c.Exists() && c.String() != "" {
|
||||||
outputs = append(outputs, map[string]interface{}{
|
item := `{"id":"","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":""}],"role":"assistant"}`
|
||||||
"id": fmt.Sprintf("msg_%s_%d", id, int(choice.Get("index").Int())),
|
item, _ = sjson.Set(item, "id", fmt.Sprintf("msg_%s_%d", id, int(choice.Get("index").Int())))
|
||||||
"type": "message",
|
item, _ = sjson.Set(item, "content.0.text", c.String())
|
||||||
"status": "completed",
|
outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", item)
|
||||||
"content": []interface{}{map[string]interface{}{
|
|
||||||
"type": "output_text",
|
|
||||||
"annotations": []interface{}{},
|
|
||||||
"logprobs": []interface{}{},
|
|
||||||
"text": c.String(),
|
|
||||||
}},
|
|
||||||
"role": "assistant",
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Function/tool calls
|
// Function/tool calls
|
||||||
@@ -736,14 +707,12 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponsesNonStream(_ context.Co
|
|||||||
callID := tc.Get("id").String()
|
callID := tc.Get("id").String()
|
||||||
name := tc.Get("function.name").String()
|
name := tc.Get("function.name").String()
|
||||||
args := tc.Get("function.arguments").String()
|
args := tc.Get("function.arguments").String()
|
||||||
outputs = append(outputs, map[string]interface{}{
|
item := `{"id":"","type":"function_call","status":"completed","arguments":"","call_id":"","name":""}`
|
||||||
"id": fmt.Sprintf("fc_%s", callID),
|
item, _ = sjson.Set(item, "id", fmt.Sprintf("fc_%s", callID))
|
||||||
"type": "function_call",
|
item, _ = sjson.Set(item, "arguments", args)
|
||||||
"status": "completed",
|
item, _ = sjson.Set(item, "call_id", callID)
|
||||||
"arguments": args,
|
item, _ = sjson.Set(item, "name", name)
|
||||||
"call_id": callID,
|
outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", item)
|
||||||
"name": name,
|
|
||||||
})
|
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -751,8 +720,8 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponsesNonStream(_ context.Co
|
|||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if len(outputs) > 0 {
|
if gjson.Get(outputsWrapper, "arr.#").Int() > 0 {
|
||||||
resp, _ = sjson.Set(resp, "output", outputs)
|
resp, _ = sjson.SetRaw(resp, "output", gjson.Get(outputsWrapper, "arr").Raw)
|
||||||
}
|
}
|
||||||
|
|
||||||
// usage mapping
|
// usage mapping
|
||||||
|
|||||||
Reference in New Issue
Block a user