feat(translate): handle canonical.ImageBlock → Anthropic image content
Picks the most efficient source shape available:
URL set → {"type":"url", "url":...}
DataBase64 set → {"type":"base64", "media_type":..., "data":...}
Path set → read file → base64 → {"type":"base64", ...}
MIME defaults to application/octet-stream when ImageBlock.MediaType is
empty (some servers accept that, some don't — operator's responsibility
to fill it in upstream).
File read failure → emits a "[image %q unavailable]" text block in its
place so the message still goes through with provenance preserved.
Verified live against Kimi K2.6 via Fabric channel: alice uploads
blue 32x32 PNG → Fabric plugin downloads + emits Media in inbound →
host turncore builds canonical.Message with ImageBlock → translator
reads file + base64s → Kimi responds "Blue".
This commit is contained in:
@@ -8,8 +8,10 @@
|
||||
package translate
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"git.hangman-lab.top/hzhang/Plexum-sdk-go/canonical"
|
||||
|
||||
@@ -74,6 +76,40 @@ func CanonicalToAnthropic(req canonical.TurnRequest, modelID string, defaultMaxT
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// imageSource converts a canonical.ImageBlock into the
|
||||
// Anthropic-shaped {"type":..., ...} source map. Picks the most
|
||||
// efficient source mode available; returns nil if all sources are
|
||||
// empty or the path is unreadable.
|
||||
//
|
||||
// URL set → {"type":"url", "url":...}
|
||||
// DataBase64 set → {"type":"base64", "media_type":..., "data":...}
|
||||
// Path set → read file → base64 → {"type":"base64", ...}
|
||||
func imageSource(v *canonical.ImageBlock) map[string]any {
|
||||
media := v.MediaType
|
||||
if media == "" {
|
||||
media = "application/octet-stream"
|
||||
}
|
||||
if v.URL != "" {
|
||||
return map[string]any{"type": "url", "url": v.URL}
|
||||
}
|
||||
if v.DataBase64 != "" {
|
||||
return map[string]any{
|
||||
"type": "base64", "media_type": media, "data": v.DataBase64,
|
||||
}
|
||||
}
|
||||
if v.Path == "" {
|
||||
return nil
|
||||
}
|
||||
raw, err := os.ReadFile(v.Path)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return map[string]any{
|
||||
"type": "base64", "media_type": media,
|
||||
"data": base64.StdEncoding.EncodeToString(raw),
|
||||
}
|
||||
}
|
||||
|
||||
func roleToAnthropic(r canonical.Role) string {
|
||||
switch r {
|
||||
case canonical.RoleUser:
|
||||
@@ -129,6 +165,16 @@ func blockToAnthropic(b canonical.Block) anthropic.ContentBlock {
|
||||
return anthropic.ContentBlock{
|
||||
"type": "thinking", "thinking": v.Thinking, "signature": v.Signature,
|
||||
}
|
||||
case *canonical.ImageBlock:
|
||||
// Three source shapes possible; emit whatever the canonical
|
||||
// block carries. Anthropic accepts base64 + url + (vendor-
|
||||
// specific) file shape. Path → read + base64 (default).
|
||||
src := imageSource(v)
|
||||
if src == nil {
|
||||
return anthropic.ContentBlock{"type": "text",
|
||||
"text": fmt.Sprintf("[image %q unavailable]", v.Path)}
|
||||
}
|
||||
return anthropic.ContentBlock{"type": "image", "source": src}
|
||||
default:
|
||||
// Best-effort generic: marshal then unmarshal into a map.
|
||||
raw, err := json.Marshal(b)
|
||||
|
||||
Reference in New Issue
Block a user