chore: initial commit
This commit is contained in:
@@ -0,0 +1,669 @@
|
||||
package protocol
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
type Kind string
|
||||
|
||||
const (
|
||||
Chat Kind = "openai-chat"
|
||||
Responses Kind = "openai-responses"
|
||||
Anthropic Kind = "anthropic"
|
||||
)
|
||||
|
||||
type Usage struct {
|
||||
Input int64 `json:"input_tokens"`
|
||||
Output int64 `json:"output_tokens"`
|
||||
CacheRead int64 `json:"cache_read_tokens"`
|
||||
CacheWrite int64 `json:"cache_write_tokens"`
|
||||
Known bool `json:"known"`
|
||||
Raw json.RawMessage `json:"raw,omitempty"`
|
||||
}
|
||||
type object = map[string]any
|
||||
|
||||
// maxJSONNodes bounds how many JSON tokens one body may contain before it is
|
||||
// materialized into maps. Without it a 16 MiB body of micro-values expands
|
||||
// into hundreds of MB of Go objects per request.
|
||||
const maxJSONNodes = 1 << 18
|
||||
|
||||
func valid(k Kind) bool { return k == Chat || k == Responses || k == Anthropic }
|
||||
|
||||
// scanNodeCount walks b with the streaming decoder and fails once more than
|
||||
// limit tokens have been seen. Malformed JSON is not an error here; the
|
||||
// materializing pass reports its usual message.
|
||||
func scanNodeCount(b []byte, limit int) error {
|
||||
d := json.NewDecoder(bytes.NewReader(b))
|
||||
n := 0
|
||||
for {
|
||||
_, err := d.Token()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
if n++; n > limit {
|
||||
return fmt.Errorf("JSON exceeds %d nodes", limit)
|
||||
}
|
||||
}
|
||||
}
|
||||
func decode(b []byte) (object, error) {
|
||||
if err := scanNodeCount(b, maxJSONNodes); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
d := json.NewDecoder(bytes.NewReader(b))
|
||||
d.UseNumber()
|
||||
var v object
|
||||
if err := d.Decode(&v); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if v == nil {
|
||||
return nil, fmt.Errorf("expected JSON object")
|
||||
}
|
||||
var extra any
|
||||
if err := d.Decode(&extra); err != io.EOF {
|
||||
return nil, fmt.Errorf("trailing JSON data")
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
func obj(v any) object { m, _ := v.(map[string]any); return m }
|
||||
func str(v any) string { s, _ := v.(string); return s }
|
||||
func arr(v any) []any { a, _ := v.([]any); return a }
|
||||
func num(v any) int64 {
|
||||
switch n := v.(type) {
|
||||
case json.Number:
|
||||
i, _ := n.Int64()
|
||||
return i
|
||||
case int64:
|
||||
return n
|
||||
case int:
|
||||
return int64(n)
|
||||
case float64:
|
||||
return int64(n)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
func fields(m object, names ...string) error {
|
||||
allowed := map[string]bool{}
|
||||
for _, n := range names {
|
||||
allowed[n] = true
|
||||
}
|
||||
for k := range m {
|
||||
if !allowed[k] {
|
||||
return fmt.Errorf("unsupported field %q", k)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func required(m object, k string) (string, error) {
|
||||
s, ok := m[k].(string)
|
||||
if !ok || s == "" {
|
||||
return "", fmt.Errorf("missing or invalid %s", k)
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
func ExtractUsage(kind Kind, body []byte) Usage {
|
||||
m, e := decode(body)
|
||||
if e != nil {
|
||||
return Usage{}
|
||||
}
|
||||
if r := obj(m["response"]); r != nil {
|
||||
m = r
|
||||
}
|
||||
if r := obj(m["message"]); r != nil {
|
||||
m = r
|
||||
}
|
||||
return usage(kind, obj(m["usage"]))
|
||||
}
|
||||
func usage(k Kind, m object) Usage {
|
||||
if m == nil {
|
||||
return Usage{}
|
||||
}
|
||||
raw, _ := json.Marshal(m)
|
||||
u := Usage{Raw: raw}
|
||||
switch k {
|
||||
case Chat:
|
||||
_, a := m["prompt_tokens"]
|
||||
_, b := m["completion_tokens"]
|
||||
u.Known = a || b
|
||||
u.Input = num(m["prompt_tokens"])
|
||||
u.Output = num(m["completion_tokens"])
|
||||
u.CacheRead = num(obj(m["prompt_tokens_details"])["cached_tokens"])
|
||||
case Responses:
|
||||
_, a := m["input_tokens"]
|
||||
_, b := m["output_tokens"]
|
||||
u.Known = a || b
|
||||
u.Input = num(m["input_tokens"])
|
||||
u.Output = num(m["output_tokens"])
|
||||
u.CacheRead = num(obj(m["input_tokens_details"])["cached_tokens"])
|
||||
case Anthropic:
|
||||
_, a := m["input_tokens"]
|
||||
_, b := m["output_tokens"]
|
||||
u.Known = a || b
|
||||
u.CacheRead = num(m["cache_read_input_tokens"])
|
||||
u.CacheWrite = num(m["cache_creation_input_tokens"])
|
||||
u.Input = num(m["input_tokens"]) + u.CacheRead + u.CacheWrite
|
||||
u.Output = num(m["output_tokens"])
|
||||
}
|
||||
return u
|
||||
}
|
||||
func usageJSON(k Kind, u Usage) object {
|
||||
switch k {
|
||||
case Chat:
|
||||
return object{"prompt_tokens": u.Input, "completion_tokens": u.Output, "total_tokens": u.Input + u.Output, "prompt_tokens_details": object{"cached_tokens": u.CacheRead}}
|
||||
case Responses:
|
||||
return object{"input_tokens": u.Input, "output_tokens": u.Output, "total_tokens": u.Input + u.Output, "input_tokens_details": object{"cached_tokens": u.CacheRead}}
|
||||
default:
|
||||
return object{"input_tokens": u.Input - u.CacheRead - u.CacheWrite, "output_tokens": u.Output, "cache_read_input_tokens": u.CacheRead, "cache_creation_input_tokens": u.CacheWrite}
|
||||
}
|
||||
}
|
||||
|
||||
type part struct{ typ, text, id, name, args, url string }
|
||||
type message struct {
|
||||
role string
|
||||
parts []part
|
||||
}
|
||||
type request struct {
|
||||
messages []message
|
||||
tools []object
|
||||
options object
|
||||
choice any
|
||||
}
|
||||
|
||||
func textPart(s string) part { return part{typ: "text", text: s} }
|
||||
|
||||
func ConvertRequest(from, to Kind, body []byte, model string) ([]byte, error) {
|
||||
if !valid(from) || !valid(to) {
|
||||
return nil, fmt.Errorf("unknown protocol")
|
||||
}
|
||||
m, err := decode(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if from == to {
|
||||
if model != "" {
|
||||
m["model"] = model
|
||||
}
|
||||
return json.Marshal(m)
|
||||
}
|
||||
r, err := readRequest(from, m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out, err := writeRequest(to, r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if model == "" {
|
||||
model = str(m["model"])
|
||||
}
|
||||
out["model"] = model
|
||||
return json.Marshal(out)
|
||||
}
|
||||
func readRequest(k Kind, m object) (request, error) {
|
||||
r := request{options: object{}}
|
||||
var err error
|
||||
common := []string{"model", "stream", "temperature", "top_p", "tools", "tool_choice"}
|
||||
switch k {
|
||||
case Chat:
|
||||
err = fields(m, append(common, "messages", "max_tokens", "max_completion_tokens", "stop", "stream_options", "parallel_tool_calls", "n")...)
|
||||
case Responses:
|
||||
err = fields(m, append(common, "input", "instructions", "max_output_tokens", "parallel_tool_calls", "store")...)
|
||||
case Anthropic:
|
||||
err = fields(m, append(common, "messages", "system", "max_tokens", "stop_sequences")...)
|
||||
}
|
||||
if err != nil {
|
||||
return r, err
|
||||
}
|
||||
if n, ok := m["n"]; ok && num(n) != 1 {
|
||||
return r, fmt.Errorf("multiple choices unsupported")
|
||||
}
|
||||
if v, ok := m["store"]; ok && v != false {
|
||||
return r, fmt.Errorf("stored Responses unsupported")
|
||||
}
|
||||
if v, ok := m["stream_options"]; ok {
|
||||
o := obj(v)
|
||||
if o == nil {
|
||||
return r, fmt.Errorf("invalid stream_options")
|
||||
}
|
||||
if e := fields(o, "include_usage"); e != nil {
|
||||
return r, e
|
||||
}
|
||||
}
|
||||
for _, key := range []string{"stream", "temperature", "top_p", "parallel_tool_calls"} {
|
||||
if v, ok := m[key]; ok {
|
||||
r.options[key] = v
|
||||
}
|
||||
}
|
||||
for _, key := range []string{"max_tokens", "max_completion_tokens", "max_output_tokens"} {
|
||||
if v, ok := m[key]; ok {
|
||||
if _, exists := r.options["max"]; exists {
|
||||
return r, fmt.Errorf("conflicting token limits")
|
||||
}
|
||||
r.options["max"] = v
|
||||
}
|
||||
}
|
||||
for _, key := range []string{"stop", "stop_sequences"} {
|
||||
if v, ok := m[key]; ok {
|
||||
if s, ok := v.(string); ok {
|
||||
v = []any{s}
|
||||
}
|
||||
if _, ok := v.([]any); !ok {
|
||||
return r, fmt.Errorf("invalid stop sequences")
|
||||
}
|
||||
r.options["stop"] = v
|
||||
}
|
||||
}
|
||||
if k == Responses {
|
||||
if s, ok := m["instructions"]; ok {
|
||||
v, ok := s.(string)
|
||||
if !ok {
|
||||
return r, fmt.Errorf("invalid instructions")
|
||||
}
|
||||
r.messages = append(r.messages, message{"system", []part{textPart(v)}})
|
||||
}
|
||||
if s, ok := m["input"].(string); ok {
|
||||
r.messages = append(r.messages, message{"user", []part{textPart(s)}})
|
||||
} else {
|
||||
items, ok := m["input"].([]any)
|
||||
if !ok {
|
||||
return r, fmt.Errorf("invalid input")
|
||||
}
|
||||
for _, v := range items {
|
||||
x := obj(v)
|
||||
typ := str(x["type"])
|
||||
switch typ {
|
||||
case "function_call":
|
||||
if e := fields(x, "type", "id", "call_id", "name", "arguments", "status"); e != nil {
|
||||
return r, e
|
||||
}
|
||||
p, e := callPart(x, "call_id", "arguments")
|
||||
if e != nil {
|
||||
return r, e
|
||||
}
|
||||
r.messages = append(r.messages, message{"assistant", []part{p}})
|
||||
case "function_call_output":
|
||||
if e := fields(x, "type", "id", "call_id", "output", "status"); e != nil {
|
||||
return r, e
|
||||
}
|
||||
id, e := required(x, "call_id")
|
||||
if e != nil {
|
||||
return r, e
|
||||
}
|
||||
s, ok := x["output"].(string)
|
||||
if !ok {
|
||||
return r, fmt.Errorf("non-text function output unsupported")
|
||||
}
|
||||
r.messages = append(r.messages, message{"tool", []part{{typ: "result", id: id, text: s}}})
|
||||
case "", "message":
|
||||
mm, e := readMessage(k, x)
|
||||
if e != nil {
|
||||
return r, e
|
||||
}
|
||||
r.messages = append(r.messages, mm)
|
||||
default:
|
||||
return r, fmt.Errorf("unsupported input item %q", typ)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if k == Anthropic {
|
||||
if s, ok := m["system"]; ok {
|
||||
ps, e := readContent(k, s)
|
||||
if e != nil {
|
||||
return r, e
|
||||
}
|
||||
for _, p := range ps {
|
||||
if p.typ != "text" {
|
||||
return r, fmt.Errorf("non-text system")
|
||||
}
|
||||
}
|
||||
r.messages = append(r.messages, message{"system", ps})
|
||||
}
|
||||
}
|
||||
ms, ok := m["messages"].([]any)
|
||||
if !ok {
|
||||
return r, fmt.Errorf("invalid messages")
|
||||
}
|
||||
for _, v := range ms {
|
||||
mm, e := readMessage(k, obj(v))
|
||||
if e != nil {
|
||||
return r, e
|
||||
}
|
||||
r.messages = append(r.messages, mm)
|
||||
}
|
||||
}
|
||||
if v, ok := m["tools"]; ok {
|
||||
ts, ok := v.([]any)
|
||||
if !ok {
|
||||
return r, fmt.Errorf("invalid tools")
|
||||
}
|
||||
for _, v := range ts {
|
||||
t := obj(v)
|
||||
var f object
|
||||
switch k {
|
||||
case Chat:
|
||||
if e := fields(t, "type", "function"); e != nil {
|
||||
return r, e
|
||||
}
|
||||
if str(t["type"]) != "function" {
|
||||
return r, fmt.Errorf("unsupported tool")
|
||||
}
|
||||
f = obj(t["function"])
|
||||
case Responses:
|
||||
if str(t["type"]) != "function" {
|
||||
return r, fmt.Errorf("unsupported tool")
|
||||
}
|
||||
f = object{}
|
||||
for key, val := range t {
|
||||
if key != "type" {
|
||||
f[key] = val
|
||||
}
|
||||
}
|
||||
case Anthropic:
|
||||
f = object{}
|
||||
for key, val := range t {
|
||||
if key == "input_schema" {
|
||||
key = "parameters"
|
||||
}
|
||||
f[key] = val
|
||||
}
|
||||
}
|
||||
if e := fields(f, "name", "description", "parameters", "strict"); e != nil {
|
||||
return r, e
|
||||
}
|
||||
if _, e := required(f, "name"); e != nil {
|
||||
return r, e
|
||||
}
|
||||
if obj(f["parameters"]) == nil {
|
||||
return r, fmt.Errorf("missing tool schema")
|
||||
}
|
||||
r.tools = append(r.tools, f)
|
||||
}
|
||||
}
|
||||
if v, ok := m["tool_choice"]; ok {
|
||||
if s, ok := v.(string); ok {
|
||||
if s != "auto" && s != "none" && s != "required" {
|
||||
return r, fmt.Errorf("unsupported tool_choice")
|
||||
}
|
||||
r.choice = s
|
||||
} else {
|
||||
x := obj(v)
|
||||
if k == Chat {
|
||||
if e := fields(x, "type", "function"); e != nil {
|
||||
return r, e
|
||||
}
|
||||
if str(x["type"]) != "function" {
|
||||
return r, fmt.Errorf("unsupported tool_choice")
|
||||
}
|
||||
f := obj(x["function"])
|
||||
if e := fields(f, "name"); e != nil {
|
||||
return r, e
|
||||
}
|
||||
s, e := required(f, "name")
|
||||
if e != nil {
|
||||
return r, e
|
||||
}
|
||||
r.choice = object{"name": s}
|
||||
} else if k == Responses {
|
||||
if e := fields(x, "type", "name"); e != nil {
|
||||
return r, e
|
||||
}
|
||||
if str(x["type"]) != "function" {
|
||||
return r, fmt.Errorf("unsupported tool_choice")
|
||||
}
|
||||
s, e := required(x, "name")
|
||||
if e != nil {
|
||||
return r, e
|
||||
}
|
||||
r.choice = object{"name": s}
|
||||
} else {
|
||||
if e := fields(x, "type", "name", "disable_parallel_tool_use"); e != nil {
|
||||
return r, e
|
||||
}
|
||||
switch str(x["type"]) {
|
||||
case "auto", "none":
|
||||
r.choice = x["type"]
|
||||
case "any":
|
||||
r.choice = "required"
|
||||
case "tool":
|
||||
s, e := required(x, "name")
|
||||
if e != nil {
|
||||
return r, e
|
||||
}
|
||||
r.choice = object{"name": s}
|
||||
default:
|
||||
return r, fmt.Errorf("unsupported tool_choice")
|
||||
}
|
||||
if b, ok := x["disable_parallel_tool_use"].(bool); ok {
|
||||
r.options["parallel_tool_calls"] = !b
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
func callPart(x object, idKey, argsKey string) (part, error) {
|
||||
id, e := required(x, idKey)
|
||||
if e != nil {
|
||||
return part{}, e
|
||||
}
|
||||
name, e := required(x, "name")
|
||||
if e != nil {
|
||||
return part{}, e
|
||||
}
|
||||
s, ok := x[argsKey].(string)
|
||||
if !ok {
|
||||
return part{}, fmt.Errorf("invalid tool arguments")
|
||||
}
|
||||
if _, e := decode([]byte(s)); e != nil {
|
||||
return part{}, fmt.Errorf("tool arguments must be JSON object: %w", e)
|
||||
}
|
||||
return part{typ: "call", id: id, name: name, args: s}, nil
|
||||
}
|
||||
func readMessage(k Kind, x object) (message, error) {
|
||||
m := message{role: str(x["role"])}
|
||||
if x == nil {
|
||||
return m, fmt.Errorf("invalid message")
|
||||
}
|
||||
if e := fields(x, "role", "content", "type", "id", "status", "tool_calls", "tool_call_id", "refusal"); e != nil {
|
||||
return m, e
|
||||
}
|
||||
if x["refusal"] != nil {
|
||||
return m, fmt.Errorf("refusal content cannot be preserved")
|
||||
}
|
||||
switch m.role {
|
||||
case "user", "assistant", "system", "developer", "tool":
|
||||
default:
|
||||
return m, fmt.Errorf("unsupported role %q", m.role)
|
||||
}
|
||||
if m.role == "tool" {
|
||||
if k != Chat {
|
||||
return m, fmt.Errorf("invalid tool role")
|
||||
}
|
||||
id, e := required(x, "tool_call_id")
|
||||
if e != nil {
|
||||
return m, e
|
||||
}
|
||||
s, ok := x["content"].(string)
|
||||
if !ok {
|
||||
return m, fmt.Errorf("non-text tool result unsupported")
|
||||
}
|
||||
m.parts = []part{{typ: "result", id: id, text: s}}
|
||||
return m, nil
|
||||
}
|
||||
if c := x["content"]; c != nil {
|
||||
ps, e := readContent(k, c)
|
||||
if e != nil {
|
||||
return m, e
|
||||
}
|
||||
m.parts = ps
|
||||
}
|
||||
if tc, ok := x["tool_calls"]; ok {
|
||||
if k != Chat || m.role != "assistant" {
|
||||
return m, fmt.Errorf("invalid tool_calls")
|
||||
}
|
||||
ts, ok := tc.([]any)
|
||||
if !ok {
|
||||
return m, fmt.Errorf("invalid tool_calls")
|
||||
}
|
||||
for _, v := range ts {
|
||||
t := obj(v)
|
||||
if e := fields(t, "id", "type", "function"); e != nil {
|
||||
return m, e
|
||||
}
|
||||
if str(t["type"]) != "function" {
|
||||
return m, fmt.Errorf("unsupported tool call")
|
||||
}
|
||||
f := obj(t["function"])
|
||||
if e := fields(f, "name", "arguments"); e != nil {
|
||||
return m, e
|
||||
}
|
||||
p, e := callPart(object{"id": t["id"], "name": f["name"], "arguments": f["arguments"]}, "id", "arguments")
|
||||
if e != nil {
|
||||
return m, e
|
||||
}
|
||||
m.parts = append(m.parts, p)
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
func readContent(k Kind, v any) ([]part, error) {
|
||||
if s, ok := v.(string); ok {
|
||||
return []part{textPart(s)}, nil
|
||||
}
|
||||
a, ok := v.([]any)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid content")
|
||||
}
|
||||
var ps []part
|
||||
for _, v := range a {
|
||||
x := obj(v)
|
||||
switch str(x["type"]) {
|
||||
case "text", "input_text", "output_text":
|
||||
if e := fields(x, "type", "text", "annotations"); e != nil {
|
||||
return nil, e
|
||||
}
|
||||
if an, ok := x["annotations"]; ok && len(arr(an)) != 0 {
|
||||
return nil, fmt.Errorf("annotations unsupported")
|
||||
}
|
||||
s, ok := x["text"].(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid text")
|
||||
}
|
||||
ps = append(ps, textPart(s))
|
||||
case "image_url":
|
||||
if k != Chat {
|
||||
return nil, fmt.Errorf("invalid image block")
|
||||
}
|
||||
if e := fields(x, "type", "image_url"); e != nil {
|
||||
return nil, e
|
||||
}
|
||||
im := obj(x["image_url"])
|
||||
if e := fields(im, "url", "detail"); e != nil {
|
||||
return nil, e
|
||||
}
|
||||
if d := str(im["detail"]); d != "" && d != "auto" {
|
||||
return nil, fmt.Errorf("image detail cannot be preserved")
|
||||
}
|
||||
s, e := required(im, "url")
|
||||
if e != nil {
|
||||
return nil, e
|
||||
}
|
||||
ps = append(ps, part{typ: "image", url: s})
|
||||
case "input_image":
|
||||
if k != Responses {
|
||||
return nil, fmt.Errorf("invalid image block")
|
||||
}
|
||||
if e := fields(x, "type", "image_url", "detail"); e != nil {
|
||||
return nil, e
|
||||
}
|
||||
if d := str(x["detail"]); d != "" && d != "auto" {
|
||||
return nil, fmt.Errorf("image detail cannot be preserved")
|
||||
}
|
||||
s, e := required(x, "image_url")
|
||||
if e != nil {
|
||||
return nil, e
|
||||
}
|
||||
ps = append(ps, part{typ: "image", url: s})
|
||||
case "image":
|
||||
if k != Anthropic {
|
||||
return nil, fmt.Errorf("invalid image block")
|
||||
}
|
||||
if e := fields(x, "type", "source"); e != nil {
|
||||
return nil, e
|
||||
}
|
||||
src := obj(x["source"])
|
||||
var url string
|
||||
switch str(src["type"]) {
|
||||
case "url":
|
||||
if e := fields(src, "type", "url"); e != nil {
|
||||
return nil, e
|
||||
}
|
||||
url = str(src["url"])
|
||||
case "base64":
|
||||
if e := fields(src, "type", "media_type", "data"); e != nil {
|
||||
return nil, e
|
||||
}
|
||||
if str(src["media_type"]) == "" || str(src["data"]) == "" {
|
||||
return nil, fmt.Errorf("invalid image source")
|
||||
}
|
||||
url = "data:" + str(src["media_type"]) + ";base64," + str(src["data"])
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported image source")
|
||||
}
|
||||
if url == "" {
|
||||
return nil, fmt.Errorf("empty image URL")
|
||||
}
|
||||
ps = append(ps, part{typ: "image", url: url})
|
||||
case "tool_use":
|
||||
if k != Anthropic {
|
||||
return nil, fmt.Errorf("invalid tool block")
|
||||
}
|
||||
if e := fields(x, "type", "id", "name", "input"); e != nil {
|
||||
return nil, e
|
||||
}
|
||||
b, e := json.Marshal(x["input"])
|
||||
if e != nil {
|
||||
return nil, e
|
||||
}
|
||||
p, e := callPart(object{"id": x["id"], "name": x["name"], "arguments": string(b)}, "id", "arguments")
|
||||
if e != nil {
|
||||
return nil, e
|
||||
}
|
||||
ps = append(ps, p)
|
||||
case "tool_result":
|
||||
if k != Anthropic {
|
||||
return nil, fmt.Errorf("invalid result block")
|
||||
}
|
||||
if e := fields(x, "type", "tool_use_id", "content", "is_error"); e != nil {
|
||||
return nil, e
|
||||
}
|
||||
if x["is_error"] == true {
|
||||
return nil, fmt.Errorf("tool error flag cannot be preserved")
|
||||
}
|
||||
id, e := required(x, "tool_use_id")
|
||||
if e != nil {
|
||||
return nil, e
|
||||
}
|
||||
s, ok := x["content"].(string)
|
||||
if !ok {
|
||||
pp, e := readContent(k, x["content"])
|
||||
if e != nil {
|
||||
return nil, e
|
||||
}
|
||||
for _, p := range pp {
|
||||
if p.typ != "text" {
|
||||
return nil, fmt.Errorf("non-text tool result unsupported")
|
||||
}
|
||||
s += p.text
|
||||
}
|
||||
}
|
||||
ps = append(ps, part{typ: "result", id: id, text: s})
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported content type %q", str(x["type"]))
|
||||
}
|
||||
}
|
||||
return ps, nil
|
||||
}
|
||||
Reference in New Issue
Block a user