| |
| |
| |
| |
| |
| |
| package claude |
|
|
| import ( |
| "bytes" |
| "context" |
| "fmt" |
| "strings" |
| "sync/atomic" |
| "time" |
|
|
| "github.com/tidwall/gjson" |
| "github.com/tidwall/sjson" |
| ) |
|
|
| |
| |
| |
| type Params struct { |
| HasFirstResponse bool |
| ResponseType int |
| ResponseIndex int |
| HasContent bool |
| } |
|
|
| |
| var toolUseIDCounter uint64 |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| func ConvertGeminiCLIResponseToClaude(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) []string { |
| if *param == nil { |
| *param = &Params{ |
| HasFirstResponse: false, |
| ResponseType: 0, |
| ResponseIndex: 0, |
| } |
| } |
|
|
| if bytes.Equal(rawJSON, []byte("[DONE]")) { |
| |
| if (*param).(*Params).HasContent { |
| return []string{ |
| "event: message_stop\ndata: {\"type\":\"message_stop\"}\n\n\n", |
| } |
| } |
| return []string{} |
| } |
|
|
| |
| usedTool := false |
| output := "" |
|
|
| |
| |
| if !(*param).(*Params).HasFirstResponse { |
| output = "event: message_start\n" |
|
|
| |
| |
| messageStartTemplate := `{"type": "message_start", "message": {"id": "msg_1nZdL29xx5MUA1yADyHTEsnR8uuvGzszyY", "type": "message", "role": "assistant", "content": [], "model": "claude-3-5-sonnet-20241022", "stop_reason": null, "stop_sequence": null, "usage": {"input_tokens": 0, "output_tokens": 0}}}` |
|
|
| |
| if modelVersionResult := gjson.GetBytes(rawJSON, "response.modelVersion"); modelVersionResult.Exists() { |
| messageStartTemplate, _ = sjson.Set(messageStartTemplate, "message.model", modelVersionResult.String()) |
| } |
| if responseIDResult := gjson.GetBytes(rawJSON, "response.responseId"); responseIDResult.Exists() { |
| messageStartTemplate, _ = sjson.Set(messageStartTemplate, "message.id", responseIDResult.String()) |
| } |
| output = output + fmt.Sprintf("data: %s\n\n\n", messageStartTemplate) |
|
|
| (*param).(*Params).HasFirstResponse = true |
| } |
|
|
| |
| |
| partsResult := gjson.GetBytes(rawJSON, "response.candidates.0.content.parts") |
| if partsResult.IsArray() { |
| partResults := partsResult.Array() |
| for i := 0; i < len(partResults); i++ { |
| partResult := partResults[i] |
|
|
| |
| partTextResult := partResult.Get("text") |
| functionCallResult := partResult.Get("functionCall") |
|
|
| |
| if partTextResult.Exists() { |
| |
| if partResult.Get("thought").Bool() { |
| |
| if (*param).(*Params).ResponseType == 2 { |
| output = output + "event: content_block_delta\n" |
| data, _ := sjson.Set(fmt.Sprintf(`{"type":"content_block_delta","index":%d,"delta":{"type":"thinking_delta","thinking":""}}`, (*param).(*Params).ResponseIndex), "delta.thinking", partTextResult.String()) |
| output = output + fmt.Sprintf("data: %s\n\n\n", data) |
| (*param).(*Params).HasContent = true |
| } else { |
| |
| |
| if (*param).(*Params).ResponseType != 0 { |
| if (*param).(*Params).ResponseType == 2 { |
| |
| |
| |
| } |
| output = output + "event: content_block_stop\n" |
| output = output + fmt.Sprintf(`data: {"type":"content_block_stop","index":%d}`, (*param).(*Params).ResponseIndex) |
| output = output + "\n\n\n" |
| (*param).(*Params).ResponseIndex++ |
| } |
|
|
| |
| output = output + "event: content_block_start\n" |
| output = output + fmt.Sprintf(`data: {"type":"content_block_start","index":%d,"content_block":{"type":"thinking","thinking":""}}`, (*param).(*Params).ResponseIndex) |
| output = output + "\n\n\n" |
| output = output + "event: content_block_delta\n" |
| data, _ := sjson.Set(fmt.Sprintf(`{"type":"content_block_delta","index":%d,"delta":{"type":"thinking_delta","thinking":""}}`, (*param).(*Params).ResponseIndex), "delta.thinking", partTextResult.String()) |
| output = output + fmt.Sprintf("data: %s\n\n\n", data) |
| (*param).(*Params).ResponseType = 2 |
| (*param).(*Params).HasContent = true |
| } |
| } else { |
| |
| |
| if (*param).(*Params).ResponseType == 1 { |
| output = output + "event: content_block_delta\n" |
| data, _ := sjson.Set(fmt.Sprintf(`{"type":"content_block_delta","index":%d,"delta":{"type":"text_delta","text":""}}`, (*param).(*Params).ResponseIndex), "delta.text", partTextResult.String()) |
| output = output + fmt.Sprintf("data: %s\n\n\n", data) |
| (*param).(*Params).HasContent = true |
| } else { |
| |
| |
| if (*param).(*Params).ResponseType != 0 { |
| if (*param).(*Params).ResponseType == 2 { |
| |
| |
| |
| } |
| output = output + "event: content_block_stop\n" |
| output = output + fmt.Sprintf(`data: {"type":"content_block_stop","index":%d}`, (*param).(*Params).ResponseIndex) |
| output = output + "\n\n\n" |
| (*param).(*Params).ResponseIndex++ |
| } |
|
|
| |
| output = output + "event: content_block_start\n" |
| output = output + fmt.Sprintf(`data: {"type":"content_block_start","index":%d,"content_block":{"type":"text","text":""}}`, (*param).(*Params).ResponseIndex) |
| output = output + "\n\n\n" |
| output = output + "event: content_block_delta\n" |
| data, _ := sjson.Set(fmt.Sprintf(`{"type":"content_block_delta","index":%d,"delta":{"type":"text_delta","text":""}}`, (*param).(*Params).ResponseIndex), "delta.text", partTextResult.String()) |
| output = output + fmt.Sprintf("data: %s\n\n\n", data) |
| (*param).(*Params).ResponseType = 1 |
| (*param).(*Params).HasContent = true |
| } |
| } |
| } else if functionCallResult.Exists() { |
| |
| |
| usedTool = true |
| fcName := functionCallResult.Get("name").String() |
|
|
| |
| |
| if (*param).(*Params).ResponseType == 3 { |
| output = output + "event: content_block_stop\n" |
| output = output + fmt.Sprintf(`data: {"type":"content_block_stop","index":%d}`, (*param).(*Params).ResponseIndex) |
| output = output + "\n\n\n" |
| (*param).(*Params).ResponseIndex++ |
| (*param).(*Params).ResponseType = 0 |
| } |
|
|
| |
| if (*param).(*Params).ResponseType == 2 { |
| |
| |
| |
| } |
|
|
| |
| if (*param).(*Params).ResponseType != 0 { |
| output = output + "event: content_block_stop\n" |
| output = output + fmt.Sprintf(`data: {"type":"content_block_stop","index":%d}`, (*param).(*Params).ResponseIndex) |
| output = output + "\n\n\n" |
| (*param).(*Params).ResponseIndex++ |
| } |
|
|
| |
| |
| output = output + "event: content_block_start\n" |
|
|
| |
| data := fmt.Sprintf(`{"type":"content_block_start","index":%d,"content_block":{"type":"tool_use","id":"","name":"","input":{}}}`, (*param).(*Params).ResponseIndex) |
| data, _ = sjson.Set(data, "content_block.id", fmt.Sprintf("%s-%d-%d", fcName, time.Now().UnixNano(), atomic.AddUint64(&toolUseIDCounter, 1))) |
| data, _ = sjson.Set(data, "content_block.name", fcName) |
| output = output + fmt.Sprintf("data: %s\n\n\n", data) |
|
|
| if fcArgsResult := functionCallResult.Get("args"); fcArgsResult.Exists() { |
| output = output + "event: content_block_delta\n" |
| data, _ = sjson.Set(fmt.Sprintf(`{"type":"content_block_delta","index":%d,"delta":{"type":"input_json_delta","partial_json":""}}`, (*param).(*Params).ResponseIndex), "delta.partial_json", fcArgsResult.Raw) |
| output = output + fmt.Sprintf("data: %s\n\n\n", data) |
| } |
| (*param).(*Params).ResponseType = 3 |
| (*param).(*Params).HasContent = true |
| } |
| } |
| } |
|
|
| usageResult := gjson.GetBytes(rawJSON, "response.usageMetadata") |
| |
| if usageResult.Exists() && bytes.Contains(rawJSON, []byte(`"finishReason"`)) { |
| if candidatesTokenCountResult := usageResult.Get("candidatesTokenCount"); candidatesTokenCountResult.Exists() { |
| |
| if (*param).(*Params).HasContent { |
| |
| output = output + "event: content_block_stop\n" |
| output = output + fmt.Sprintf(`data: {"type":"content_block_stop","index":%d}`, (*param).(*Params).ResponseIndex) |
| output = output + "\n\n\n" |
|
|
| |
| output = output + "event: message_delta\n" |
| output = output + `data: ` |
|
|
| |
| template := `{"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"input_tokens":0,"output_tokens":0}}` |
| |
| if usedTool { |
| template = `{"type":"message_delta","delta":{"stop_reason":"tool_use","stop_sequence":null},"usage":{"input_tokens":0,"output_tokens":0}}` |
| } |
|
|
| |
| thoughtsTokenCount := usageResult.Get("thoughtsTokenCount").Int() |
| template, _ = sjson.Set(template, "usage.output_tokens", candidatesTokenCountResult.Int()+thoughtsTokenCount) |
| template, _ = sjson.Set(template, "usage.input_tokens", usageResult.Get("promptTokenCount").Int()) |
|
|
| output = output + template + "\n\n\n" |
| } |
| } |
| } |
|
|
| return []string{output} |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| func ConvertGeminiCLIResponseToClaudeNonStream(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) string { |
| _ = originalRequestRawJSON |
| _ = requestRawJSON |
|
|
| 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("response.responseId").String()) |
| out, _ = sjson.Set(out, "model", root.Get("response.modelVersion").String()) |
|
|
| inputTokens := root.Get("response.usageMetadata.promptTokenCount").Int() |
| outputTokens := root.Get("response.usageMetadata.candidatesTokenCount").Int() + root.Get("response.usageMetadata.thoughtsTokenCount").Int() |
| out, _ = sjson.Set(out, "usage.input_tokens", inputTokens) |
| out, _ = sjson.Set(out, "usage.output_tokens", outputTokens) |
|
|
| parts := root.Get("response.candidates.0.content.parts") |
| textBuilder := strings.Builder{} |
| thinkingBuilder := strings.Builder{} |
| toolIDCounter := 0 |
| hasToolCall := false |
|
|
| flushText := func() { |
| if textBuilder.Len() == 0 { |
| return |
| } |
| block := `{"type":"text","text":""}` |
| block, _ = sjson.Set(block, "text", textBuilder.String()) |
| out, _ = sjson.SetRaw(out, "content.-1", block) |
| textBuilder.Reset() |
| } |
|
|
| flushThinking := func() { |
| if thinkingBuilder.Len() == 0 { |
| return |
| } |
| block := `{"type":"thinking","thinking":""}` |
| block, _ = sjson.Set(block, "thinking", thinkingBuilder.String()) |
| out, _ = sjson.SetRaw(out, "content.-1", block) |
| thinkingBuilder.Reset() |
| } |
|
|
| if parts.IsArray() { |
| for _, part := range parts.Array() { |
| if text := part.Get("text"); text.Exists() && text.String() != "" { |
| if part.Get("thought").Bool() { |
| flushText() |
| thinkingBuilder.WriteString(text.String()) |
| continue |
| } |
| flushThinking() |
| textBuilder.WriteString(text.String()) |
| continue |
| } |
|
|
| if functionCall := part.Get("functionCall"); functionCall.Exists() { |
| flushThinking() |
| flushText() |
| hasToolCall = true |
|
|
| name := functionCall.Get("name").String() |
| toolIDCounter++ |
| toolBlock := `{"type":"tool_use","id":"","name":"","input":{}}` |
| toolBlock, _ = sjson.Set(toolBlock, "id", fmt.Sprintf("tool_%d", toolIDCounter)) |
| toolBlock, _ = sjson.Set(toolBlock, "name", name) |
| inputRaw := "{}" |
| if args := functionCall.Get("args"); args.Exists() && gjson.Valid(args.Raw) && args.IsObject() { |
| inputRaw = args.Raw |
| } |
| toolBlock, _ = sjson.SetRaw(toolBlock, "input", inputRaw) |
| out, _ = sjson.SetRaw(out, "content.-1", toolBlock) |
| continue |
| } |
| } |
| } |
|
|
| flushThinking() |
| flushText() |
|
|
| stopReason := "end_turn" |
| if hasToolCall { |
| stopReason = "tool_use" |
| } else { |
| if finish := root.Get("response.candidates.0.finishReason"); finish.Exists() { |
| switch finish.String() { |
| case "MAX_TOKENS": |
| stopReason = "max_tokens" |
| case "STOP", "FINISH_REASON_UNSPECIFIED", "UNKNOWN": |
| stopReason = "end_turn" |
| default: |
| stopReason = "end_turn" |
| } |
| } |
| } |
| out, _ = sjson.Set(out, "stop_reason", stopReason) |
|
|
| if inputTokens == int64(0) && outputTokens == int64(0) && !root.Get("response.usageMetadata").Exists() { |
| out, _ = sjson.Delete(out, "usage") |
| } |
|
|
| return out |
| } |
|
|
| func ClaudeTokenCount(ctx context.Context, count int64) string { |
| return fmt.Sprintf(`{"input_tokens":%d}`, count) |
| } |
|
|