|
|
|
@@ -8,6 +8,7 @@ import ( |
|
|
|
"io" |
|
|
|
"net/http" |
|
|
|
"net/url" |
|
|
|
"sort" |
|
|
|
"strings" |
|
|
|
"time" |
|
|
|
) |
|
|
|
@@ -86,20 +87,15 @@ func (c *openAICompatibleClient) Generate(ctx context.Context, req Request) (str |
|
|
|
return "", err |
|
|
|
} |
|
|
|
|
|
|
|
var response struct { |
|
|
|
Choices []struct { |
|
|
|
Message struct { |
|
|
|
Content string `json:"content"` |
|
|
|
} `json:"message"` |
|
|
|
} `json:"choices"` |
|
|
|
} |
|
|
|
var response map[string]any |
|
|
|
if err := json.Unmarshal(body, &response); err != nil { |
|
|
|
return "", fmt.Errorf("decode openai-compatible response: %w", err) |
|
|
|
} |
|
|
|
if len(response.Choices) == 0 { |
|
|
|
return "", fmt.Errorf("empty openai-compatible response") |
|
|
|
content := extractOpenAICompatibleContent(response) |
|
|
|
if content == "" { |
|
|
|
return "", fmt.Errorf("empty openai-compatible response content (%s)", describeOpenAICompatibleShape(response)) |
|
|
|
} |
|
|
|
return strings.TrimSpace(response.Choices[0].Message.Content), nil |
|
|
|
return content, nil |
|
|
|
} |
|
|
|
|
|
|
|
type anthropicClient struct { |
|
|
|
@@ -327,3 +323,162 @@ func nestedString(values map[string]any, path ...string) string { |
|
|
|
return "" |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
func extractOpenAICompatibleContent(response map[string]any) string { |
|
|
|
if response == nil { |
|
|
|
return "" |
|
|
|
} |
|
|
|
if text := strings.TrimSpace(extractOpenAICompatibleChoicesContent(response["choices"])); text != "" { |
|
|
|
return text |
|
|
|
} |
|
|
|
if text := strings.TrimSpace(extractTextFromContentValue(response["output_text"])); text != "" { |
|
|
|
return text |
|
|
|
} |
|
|
|
return strings.TrimSpace(extractOpenAICompatibleOutputContent(response["output"])) |
|
|
|
} |
|
|
|
|
|
|
|
func extractOpenAICompatibleChoicesContent(raw any) string { |
|
|
|
choices, ok := raw.([]any) |
|
|
|
if !ok { |
|
|
|
return "" |
|
|
|
} |
|
|
|
for _, rawChoice := range choices { |
|
|
|
choice, ok := rawChoice.(map[string]any) |
|
|
|
if !ok { |
|
|
|
continue |
|
|
|
} |
|
|
|
if text := strings.TrimSpace(extractTextFromContentValue(choice["message"])); text != "" { |
|
|
|
return text |
|
|
|
} |
|
|
|
if text := strings.TrimSpace(extractTextFromContentValue(choice["delta"])); text != "" { |
|
|
|
return text |
|
|
|
} |
|
|
|
if text := strings.TrimSpace(extractTextFromContentValue(choice["text"])); text != "" { |
|
|
|
return text |
|
|
|
} |
|
|
|
} |
|
|
|
return "" |
|
|
|
} |
|
|
|
|
|
|
|
func extractOpenAICompatibleOutputContent(raw any) string { |
|
|
|
output, ok := raw.([]any) |
|
|
|
if !ok { |
|
|
|
return "" |
|
|
|
} |
|
|
|
for _, rawItem := range output { |
|
|
|
item, ok := rawItem.(map[string]any) |
|
|
|
if !ok { |
|
|
|
continue |
|
|
|
} |
|
|
|
if text := strings.TrimSpace(extractTextFromContentValue(item["content"])); text != "" { |
|
|
|
return text |
|
|
|
} |
|
|
|
if text := strings.TrimSpace(extractTextFromContentValue(item["text"])); text != "" { |
|
|
|
return text |
|
|
|
} |
|
|
|
} |
|
|
|
return "" |
|
|
|
} |
|
|
|
|
|
|
|
func extractTextFromContentValue(raw any) string { |
|
|
|
switch value := raw.(type) { |
|
|
|
case string: |
|
|
|
return strings.TrimSpace(value) |
|
|
|
case []any: |
|
|
|
parts := make([]string, 0, len(value)) |
|
|
|
for _, item := range value { |
|
|
|
if text := strings.TrimSpace(extractTextFromContentValue(item)); text != "" { |
|
|
|
parts = append(parts, text) |
|
|
|
} |
|
|
|
} |
|
|
|
return strings.TrimSpace(strings.Join(parts, "\n")) |
|
|
|
case map[string]any: |
|
|
|
if text := strings.TrimSpace(extractTextFromContentValue(value["content"])); text != "" { |
|
|
|
return text |
|
|
|
} |
|
|
|
if text := strings.TrimSpace(extractTextFromContentValue(value["text"])); text != "" { |
|
|
|
return text |
|
|
|
} |
|
|
|
if text := strings.TrimSpace(extractTextFromContentValue(value["value"])); text != "" { |
|
|
|
return text |
|
|
|
} |
|
|
|
if text := strings.TrimSpace(extractTextFromContentValue(value["output_text"])); text != "" { |
|
|
|
return text |
|
|
|
} |
|
|
|
return "" |
|
|
|
default: |
|
|
|
return "" |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
func describeOpenAICompatibleShape(response map[string]any) string { |
|
|
|
parts := make([]string, 0, 8) |
|
|
|
parts = append(parts, "top="+describeMapKeys(response)) |
|
|
|
|
|
|
|
if choices, ok := response["choices"].([]any); ok { |
|
|
|
parts = append(parts, fmt.Sprintf("choices_len=%d", len(choices))) |
|
|
|
if len(choices) > 0 { |
|
|
|
if choice, ok := choices[0].(map[string]any); ok { |
|
|
|
parts = append(parts, "choices0="+describeMapKeys(choice)) |
|
|
|
if message, ok := choice["message"].(map[string]any); ok { |
|
|
|
parts = append(parts, "message="+describeMapKeys(message)) |
|
|
|
parts = append(parts, "message_content_type="+valueType(message["content"])) |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} else if _, exists := response["choices"]; exists { |
|
|
|
parts = append(parts, "choices_type="+valueType(response["choices"])) |
|
|
|
} |
|
|
|
|
|
|
|
if _, exists := response["output_text"]; exists { |
|
|
|
parts = append(parts, "output_text_type="+valueType(response["output_text"])) |
|
|
|
} |
|
|
|
if output, ok := response["output"].([]any); ok { |
|
|
|
parts = append(parts, fmt.Sprintf("output_len=%d", len(output))) |
|
|
|
if len(output) > 0 { |
|
|
|
if first, ok := output[0].(map[string]any); ok { |
|
|
|
parts = append(parts, "output0="+describeMapKeys(first)) |
|
|
|
parts = append(parts, "output0_content_type="+valueType(first["content"])) |
|
|
|
} |
|
|
|
} |
|
|
|
} else if _, exists := response["output"]; exists { |
|
|
|
parts = append(parts, "output_type="+valueType(response["output"])) |
|
|
|
} |
|
|
|
|
|
|
|
return strings.Join(parts, "; ") |
|
|
|
} |
|
|
|
|
|
|
|
func describeMapKeys(raw map[string]any) string { |
|
|
|
if len(raw) == 0 { |
|
|
|
return "{}" |
|
|
|
} |
|
|
|
keys := make([]string, 0, len(raw)) |
|
|
|
for key := range raw { |
|
|
|
keys = append(keys, key) |
|
|
|
} |
|
|
|
sort.Strings(keys) |
|
|
|
described := make([]string, 0, len(keys)) |
|
|
|
for _, key := range keys { |
|
|
|
described = append(described, fmt.Sprintf("%s:%s", key, valueType(raw[key]))) |
|
|
|
} |
|
|
|
return "{" + strings.Join(described, ",") + "}" |
|
|
|
} |
|
|
|
|
|
|
|
func valueType(raw any) string { |
|
|
|
switch raw.(type) { |
|
|
|
case nil: |
|
|
|
return "null" |
|
|
|
case string: |
|
|
|
return "string" |
|
|
|
case bool: |
|
|
|
return "bool" |
|
|
|
case float64: |
|
|
|
return "number" |
|
|
|
case []any: |
|
|
|
return "array" |
|
|
|
case map[string]any: |
|
|
|
return "object" |
|
|
|
default: |
|
|
|
return fmt.Sprintf("%T", raw) |
|
|
|
} |
|
|
|
} |