Skip to content

Commit 30c3e4d

Browse files
committed
init
Signed-off-by: yxia216 <[email protected]>
1 parent 874b0a5 commit 30c3e4d

File tree

5 files changed

+307
-24
lines changed

5 files changed

+307
-24
lines changed

internal/apischema/openai/openai.go

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ package openai
1010

1111
import (
1212
"bytes"
13+
"encoding/base64"
1314
"encoding/json"
1415
"errors"
1516
"fmt"
@@ -515,8 +516,8 @@ type ChatCompletionAssistantMessageParamContent struct {
515516
Text *string `json:"text,omitempty"`
516517

517518
// The signature for a thinking block.
518-
Signature *string `json:"signature,omitempty"`
519-
RedactedContent []byte `json:"redactedContent,omitempty"`
519+
Signature *string `json:"signature,omitempty"`
520+
RedactedContent *RedactedContentUnion `json:"redactedContent,omitempty"`
520521
*AnthropicContentFields `json:",inline,omitempty"`
521522
}
522523

@@ -1583,6 +1584,43 @@ func (e EmbeddingUnion) MarshalJSON() ([]byte, error) {
15831584
return json.Marshal(e.Value)
15841585
}
15851586

1587+
// RedactedContentUnion is a union type that can handle both []byte and string formats.
1588+
// AWS Bedrock uses []byte while GCP Anthropic uses string.
1589+
type RedactedContentUnion struct {
1590+
Value any
1591+
}
1592+
1593+
// UnmarshalJSON implements json.Unmarshaler to handle both []byte and string formats.
1594+
func (r *RedactedContentUnion) UnmarshalJSON(data []byte) error {
1595+
// Try to unmarshal as []byte first (base64 encoded).
1596+
var str string
1597+
if err := json.Unmarshal(data, &str); err == nil {
1598+
// Try to decode as base64 first (this would be []byte encoded as base64)
1599+
if decoded, err := base64.StdEncoding.DecodeString(str); err == nil {
1600+
r.Value = decoded
1601+
return nil
1602+
}
1603+
// If not base64, treat as plain string
1604+
r.Value = str
1605+
return nil
1606+
}
1607+
1608+
return errors.New("redactedContent must be either []byte (base64 encoded) or string")
1609+
}
1610+
1611+
// MarshalJSON implements json.Marshaler.
1612+
func (r RedactedContentUnion) MarshalJSON() ([]byte, error) {
1613+
switch v := r.Value.(type) {
1614+
case []byte:
1615+
// Encode []byte as base64 string
1616+
return json.Marshal(base64.StdEncoding.EncodeToString(v))
1617+
case string:
1618+
return json.Marshal(v)
1619+
default:
1620+
return json.Marshal(r.Value)
1621+
}
1622+
}
1623+
15861624
// EmbeddingUsage represents the usage information for an embeddings request.
15871625
// https://platform.openai.com/docs/api-reference/embeddings/object#embeddings/object-usage
15881626
type EmbeddingUsage struct {

internal/translator/openai_awsbedrock.go

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -312,11 +312,18 @@ func (o *openAIToAWSBedrockTranslatorV1ChatCompletion) openAIMessageToBedrockMes
312312
}
313313
case openai.ChatCompletionAssistantMessageParamContentTypeRedactedThinking:
314314
if content.RedactedContent != nil {
315-
contentBlocks = append(contentBlocks, &awsbedrock.ContentBlock{
316-
ReasoningContent: &awsbedrock.ReasoningContentBlock{
317-
RedactedContent: content.RedactedContent,
318-
},
319-
})
315+
switch v := content.RedactedContent.Value.(type) {
316+
case []byte:
317+
contentBlocks = append(contentBlocks, &awsbedrock.ContentBlock{
318+
ReasoningContent: &awsbedrock.ReasoningContentBlock{
319+
RedactedContent: v,
320+
},
321+
})
322+
case string:
323+
return nil, fmt.Errorf("AWS Bedrock does not support string format for RedactedContent, expected []byte")
324+
default:
325+
return nil, fmt.Errorf("unsupported RedactedContent type: %T, expected []byte", v)
326+
}
320327
}
321328
case openai.ChatCompletionAssistantMessageParamContentTypeRefusal:
322329
if content.Refusal != nil {

internal/translator/openai_awsbedrock_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -986,7 +986,7 @@ func TestOpenAIToAWSBedrockTranslatorV1ChatCompletion_RequestBody(t *testing.T)
986986
Value: []openai.ChatCompletionAssistantMessageParamContent{
987987
{
988988
Type: openai.ChatCompletionAssistantMessageParamContentTypeRedactedThinking,
989-
RedactedContent: []byte{104, 101, 108, 108, 111},
989+
RedactedContent: &openai.RedactedContentUnion{Value: []byte{104, 101, 108, 108, 111}},
990990
},
991991
},
992992
},

internal/translator/openai_gcpanthropic.go

Lines changed: 57 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -375,29 +375,71 @@ func anthropicRoleToOpenAIRole(role anthropic.MessageParamRole) (string, error)
375375
}
376376
}
377377

378+
// processAssistantContent processes a single ChatCompletionAssistantMessageParamContent and returns the corresponding Anthropic content block.
379+
func processAssistantContent(content openai.ChatCompletionAssistantMessageParamContent) (*anthropic.ContentBlockParamUnion, error) {
380+
switch content.Type {
381+
case openai.ChatCompletionAssistantMessageParamContentTypeRefusal:
382+
if content.Refusal != nil {
383+
block := anthropic.NewTextBlock(*content.Refusal)
384+
return &block, nil
385+
}
386+
case openai.ChatCompletionAssistantMessageParamContentTypeText:
387+
if content.Text != nil {
388+
textBlock := anthropic.NewTextBlock(*content.Text)
389+
if isCacheEnabled(content.AnthropicContentFields) {
390+
textBlock.OfText.CacheControl = content.CacheControl
391+
}
392+
return &textBlock, nil
393+
}
394+
case openai.ChatCompletionAssistantMessageParamContentTypeThinking:
395+
// thinking can not be cached: https://platform.claude.com/docs/en/build-with-claude/prompt-caching
396+
if content.Text != nil && content.Signature != nil {
397+
thinkBlock := anthropic.NewThinkingBlock(*content.Text, *content.Signature)
398+
return &thinkBlock, nil
399+
}
400+
case openai.ChatCompletionAssistantMessageParamContentTypeRedactedThinking:
401+
if content.RedactedContent != nil {
402+
switch v := content.RedactedContent.Value.(type) {
403+
case string:
404+
redactedThinkingBlock := anthropic.NewRedactedThinkingBlock(v)
405+
return &redactedThinkingBlock, nil
406+
case []byte:
407+
return nil, fmt.Errorf("GCP Anthropic does not support []byte format for RedactedContent, expected string")
408+
default:
409+
return nil, fmt.Errorf("unsupported RedactedContent type: %T, expected string", v)
410+
}
411+
}
412+
default:
413+
return nil, fmt.Errorf("content type not supported: %v", content.Type)
414+
}
415+
return nil, nil
416+
}
417+
378418
// openAIMessageToAnthropicMessageRoleAssistant converts an OpenAI assistant message to Anthropic content blocks.
379419
// The tool_use content is appended to the Anthropic message content list if tool_calls are present.
380420
func openAIMessageToAnthropicMessageRoleAssistant(openAiMessage *openai.ChatCompletionAssistantMessageParam) (anthropicMsg anthropic.MessageParam, err error) {
381421
contentBlocks := make([]anthropic.ContentBlockParamUnion, 0)
382422
if v, ok := openAiMessage.Content.Value.(string); ok && len(v) > 0 {
383423
contentBlocks = append(contentBlocks, anthropic.NewTextBlock(v))
384424
} else if content, ok := openAiMessage.Content.Value.(openai.ChatCompletionAssistantMessageParamContent); ok {
385-
switch content.Type {
386-
case openai.ChatCompletionAssistantMessageParamContentTypeRefusal:
387-
if content.Refusal != nil {
388-
contentBlocks = append(contentBlocks, anthropic.NewTextBlock(*content.Refusal))
389-
}
390-
case openai.ChatCompletionAssistantMessageParamContentTypeText:
391-
if content.Text != nil {
392-
textBlock := anthropic.NewTextBlock(*content.Text)
393-
if isCacheEnabled(content.AnthropicContentFields) {
394-
textBlock.OfText.CacheControl = content.CacheControl
395-
}
396-
contentBlocks = append(contentBlocks, textBlock)
425+
// Handle single content object
426+
var block *anthropic.ContentBlockParamUnion
427+
block, err = processAssistantContent(content)
428+
if err != nil {
429+
return anthropicMsg, err
430+
} else if block != nil {
431+
contentBlocks = append(contentBlocks, *block)
432+
}
433+
} else if contents, ok := openAiMessage.Content.Value.([]openai.ChatCompletionAssistantMessageParamContent); ok {
434+
// Handle array of content objects
435+
for _, content := range contents {
436+
var block *anthropic.ContentBlockParamUnion
437+
block, err = processAssistantContent(content)
438+
if err != nil {
439+
return anthropicMsg, err
440+
} else if block != nil {
441+
contentBlocks = append(contentBlocks, *block)
397442
}
398-
default:
399-
err = fmt.Errorf("content type not supported: %v", content.Type)
400-
return
401443
}
402444
}
403445

0 commit comments

Comments
 (0)