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
|
package translate
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
"git.hangman-lab.top/hzhang/Plexum-sdk-go/canonical"
|
"git.hangman-lab.top/hzhang/Plexum-sdk-go/canonical"
|
||||||
|
|
||||||
@@ -74,6 +76,40 @@ func CanonicalToAnthropic(req canonical.TurnRequest, modelID string, defaultMaxT
|
|||||||
return out, nil
|
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 {
|
func roleToAnthropic(r canonical.Role) string {
|
||||||
switch r {
|
switch r {
|
||||||
case canonical.RoleUser:
|
case canonical.RoleUser:
|
||||||
@@ -129,6 +165,16 @@ func blockToAnthropic(b canonical.Block) anthropic.ContentBlock {
|
|||||||
return anthropic.ContentBlock{
|
return anthropic.ContentBlock{
|
||||||
"type": "thinking", "thinking": v.Thinking, "signature": v.Signature,
|
"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:
|
default:
|
||||||
// Best-effort generic: marshal then unmarshal into a map.
|
// Best-effort generic: marshal then unmarshal into a map.
|
||||||
raw, err := json.Marshal(b)
|
raw, err := json.Marshal(b)
|
||||||
|
|||||||
Reference in New Issue
Block a user