mirror of
https://github.com/ollama/ollama.git
synced 2026-09-21 13:38:14 -05:00
openai: accept plaintext-labeled Codex agent messages (#18329)
This commit is contained in:
@@ -0,0 +1,205 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/ollama/ollama/openai"
|
||||
)
|
||||
|
||||
func TestNormalizeOllamaAgentMessagesPreservesConversation(t *testing.T) {
|
||||
body := []byte(`{"model":"test:cloud","input":[
|
||||
{"type":"message","role":"developer","content":"Environment"},
|
||||
{"type":"agent_message","id":"amsg_initial","author":"/root","recipient":"/root/child","content":[{"type":"input_text","text":"Read the "},{"type":"input_text","text":"fixture.\n"}],"internal_chat_message_metadata_passthrough":{"turn_id":"initial-turn"}},
|
||||
{"type":"function_call","call_id":"call_read","name":"read_file","arguments":"{}"},
|
||||
{"type":"function_call_output","call_id":"call_read","output":"fixture contents"},
|
||||
{"type":"agent_message","author":"/root/child","recipient":"/root","content":[{"type":"input_text","text":"Task finished."}]},
|
||||
{"type":"agent_message","author":"/root","recipient":"/root/child","content":[{"type":"input_text","text":"Now return RCA_FOLLOWUP."}]}
|
||||
]}`)
|
||||
got, err := normalizeOllamaRequestBody(body, routingModel{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var request openai.ResponsesRequest
|
||||
if err := json.Unmarshal(got, &request); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
chat, err := openai.FromResponsesRequest(request)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
wantRoles := []string{"system", "user", "assistant", "tool", "user", "user"}
|
||||
wantContent := []string{"Environment", "Agent message from \"/root\" to \"/root/child\":\nRead the fixture.\n", "", "fixture contents", "Agent message from \"/root/child\" to \"/root\":\nTask finished.", "Agent message from \"/root\" to \"/root/child\":\nNow return RCA_FOLLOWUP."}
|
||||
if len(chat.Messages) != len(wantRoles) {
|
||||
t.Fatalf("got %d messages: %+v", len(chat.Messages), chat.Messages)
|
||||
}
|
||||
for i, msg := range chat.Messages {
|
||||
if msg.Role != wantRoles[i] || msg.Content != wantContent[i] {
|
||||
t.Errorf("message %d = %q %q; want %q %q", i, msg.Role, msg.Content, wantRoles[i], wantContent[i])
|
||||
}
|
||||
}
|
||||
if len(chat.Messages[2].ToolCalls) != 1 || chat.Messages[2].ToolCalls[0].ID != "call_read" || chat.Messages[3].ToolCallID != "call_read" {
|
||||
t.Fatal("tool call pairing changed")
|
||||
}
|
||||
var payload struct {
|
||||
Input []map[string]json.RawMessage `json:"input"`
|
||||
}
|
||||
if err := json.Unmarshal(got, &payload); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(payload.Input[1]["id"]) != `"amsg_initial"` || string(payload.Input[1]["internal_chat_message_metadata_passthrough"]) != `{"turn_id":"initial-turn"}` {
|
||||
t.Fatalf("message metadata changed: %s", got)
|
||||
}
|
||||
again, err := normalizeOllamaRequestBody(got, routingModel{})
|
||||
if err != nil || !bytes.Equal(got, again) {
|
||||
t.Fatalf("normalization is not idempotent: %s, %v", again, err)
|
||||
}
|
||||
native, changed, err := normalizeNativeRequestBody(body)
|
||||
if err != nil || changed || !bytes.Equal(native, body) {
|
||||
t.Fatalf("native conversation changed: %s, %v", native, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeOllamaAgentMessageRejectsIncompleteContent(t *testing.T) {
|
||||
for _, tt := range []struct{ name, content, want string }{
|
||||
{"unknown", `[{"type":"new_content","text":"secret-task"}]`, "unsupported Codex agent message content type"},
|
||||
{"missing type", `[{"text":"secret-task"}]`, "unsupported Codex agent message content type"},
|
||||
{"missing text", `[{"type":"input_text"}]`, "requires text"},
|
||||
{"null text", `[{"type":"input_text","text":null}]`, "requires text"},
|
||||
{"non-string text", `[{"type":"input_text","text":42}]`, "decode Codex agent message content"},
|
||||
{"empty", `[]`, "requires author, recipient, and content"},
|
||||
{"null", `null`, "requires author, recipient, and content"},
|
||||
{"not array", `"secret-task"`, "decode Codex agent message"},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
item := []byte(fmt.Sprintf(`{"type":"agent_message","author":"/root","recipient":"/root/child","content":%s}`, tt.content))
|
||||
got, keep, err := normalizeOllamaInputItem(item)
|
||||
if err == nil || !strings.Contains(err.Error(), tt.want) || keep || got != nil {
|
||||
t.Fatalf("got %s, %v, %v", got, keep, err)
|
||||
}
|
||||
if strings.Contains(err.Error(), "secret-") {
|
||||
t.Fatalf("content exposed in error: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
for _, field := range []string{"author", "recipient"} {
|
||||
t.Run("missing "+field, func(t *testing.T) {
|
||||
item := map[string]any{"type": "agent_message", "author": "/root", "recipient": "/root/child", "content": []any{map[string]string{"type": "input_text", "text": "task"}}}
|
||||
delete(item, field)
|
||||
raw, _ := json.Marshal(item)
|
||||
if _, _, err := normalizeOllamaInputItem(raw); err == nil {
|
||||
t.Fatalf("accepted message without %s", field)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeOllamaAgentMessageAcceptsEncryptedContentAsText(t *testing.T) {
|
||||
item := []byte(`{"type":"agent_message","author":"/root","recipient":"/root/child","content":[{"type":"input_text","text":"Payload:\n"},{"type":"encrypted_content","encrypted_content":"secret-task"},{"type":"input_text","text":"trailing instruction"}]}`)
|
||||
got, keep, err := normalizeOllamaInputItem(item)
|
||||
if err != nil || !keep {
|
||||
t.Fatalf("normalize failed: %v", err)
|
||||
}
|
||||
var msg struct {
|
||||
Type string `json:"type"`
|
||||
Role string `json:"role"`
|
||||
Content []struct {
|
||||
Type string `json:"type"`
|
||||
Text string `json:"text"`
|
||||
} `json:"content"`
|
||||
}
|
||||
if err := json.Unmarshal(got, &msg); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if msg.Type != "message" || msg.Role != "user" {
|
||||
t.Fatalf("converted message = %q %q", msg.Type, msg.Role)
|
||||
}
|
||||
want := []struct{ Type, Text string }{
|
||||
{"input_text", "Agent message from \"/root\" to \"/root/child\":\n"},
|
||||
{"input_text", "Payload:\n"},
|
||||
{"input_text", "secret-task"},
|
||||
{"input_text", "trailing instruction"},
|
||||
}
|
||||
if len(msg.Content) != len(want) {
|
||||
t.Fatalf("content parts = %+v", msg.Content)
|
||||
}
|
||||
for i, part := range msg.Content {
|
||||
if part.Type != want[i].Type || part.Text != want[i].Text {
|
||||
t.Errorf("part %d = %q %q; want %q %q", i, part.Type, part.Text, want[i].Type, want[i].Text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCodexDesktopEncryptedAgentMessageReachesOllamaAsText(t *testing.T) {
|
||||
var bodies [][]byte
|
||||
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
bodies = append(bodies, body)
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}))
|
||||
defer upstream.Close()
|
||||
h := newTestCodexDesktop(t, upstream.URL, upstream.URL, writeCatalog(t, "glm-5.3-flash:cloud", "glm-5.3:cloud"))
|
||||
for _, model := range []string{"glm-5.3:cloud", "gpt-5.6-terra"} {
|
||||
t.Run(model, func(t *testing.T) {
|
||||
body := fmt.Sprintf(`{"model":%q,"input":[{"type":"message","role":"user","content":"old task"},{"type":"agent_message","author":"/root","recipient":"/root/child","content":[{"type":"input_text","text":"Payload:"},{"type":"encrypted_content","encrypted_content":"secret-cipher"}]}]}`, model)
|
||||
req := httptest.NewRequest("POST", CodexDesktopPathPrefix+"/v1/responses", strings.NewReader(body))
|
||||
req.RemoteAddr = "127.0.0.1:1234"
|
||||
req.Header.Set("Authorization", "Bearer native-test")
|
||||
req.Header.Set("ChatGPT-Account-ID", "test-account")
|
||||
w := httptest.NewRecorder()
|
||||
h.ServeHTTP(w, req)
|
||||
if w.Code != 204 {
|
||||
t.Fatalf("request = %d: %s", w.Code, w.Body)
|
||||
}
|
||||
forwarded := bodies[len(bodies)-1]
|
||||
if !bytes.Contains(forwarded, []byte("secret-cipher")) {
|
||||
t.Errorf("payload lost: %s", forwarded)
|
||||
}
|
||||
if model == "gpt-5.6-terra" {
|
||||
if !bytes.Contains(forwarded, []byte(`"type":"agent_message"`)) {
|
||||
t.Errorf("native transcript changed: %s", forwarded)
|
||||
}
|
||||
return
|
||||
}
|
||||
if bytes.Contains(forwarded, []byte(`"type":"agent_message"`)) {
|
||||
t.Errorf("agent message not converted for Ollama: %s", forwarded)
|
||||
}
|
||||
if !bytes.Contains(forwarded, []byte(`\"type\":\"input_text\",\"text\":\"secret-cipher\"`)) &&
|
||||
!bytes.Contains(forwarded, []byte(`"text":"secret-cipher"`)) {
|
||||
t.Errorf("encrypted payload not flattened to text: %s", forwarded)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentMessageEnvelopeMatchesOpenAI(t *testing.T) {
|
||||
item := []byte(`{"type":"agent_message","author":"/root","recipient":"/root/child","content":[{"type":"input_text","text":"task"}]}`)
|
||||
got, keep, err := normalizeOllamaInputItem(item)
|
||||
if err != nil || !keep {
|
||||
t.Fatalf("normalize failed: %v", err)
|
||||
}
|
||||
var msg struct {
|
||||
Content []struct {
|
||||
Text string `json:"text"`
|
||||
} `json:"content"`
|
||||
}
|
||||
if err := json.Unmarshal(got, &msg); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(msg.Content) == 0 {
|
||||
t.Fatal("no content parts")
|
||||
}
|
||||
want := fmt.Sprintf(openai.AgentMessageEnvelopeFormat, "/root", "/root/child")
|
||||
if msg.Content[0].Text != want {
|
||||
t.Fatalf("envelope = %q; want %q (keep the proxy envelope in sync with openai.AgentMessageEnvelopeFormat)", msg.Content[0].Text, want)
|
||||
}
|
||||
}
|
||||
@@ -344,6 +344,74 @@ func normalizeOllamaInputItem(item json.RawMessage) (json.RawMessage, bool, erro
|
||||
return nil, false, fmt.Errorf("encode system message: %w", err)
|
||||
}
|
||||
return converted, true, nil
|
||||
case "agent_message":
|
||||
var message struct {
|
||||
Author string `json:"author"`
|
||||
Recipient string `json:"recipient"`
|
||||
Content []json.RawMessage `json:"content"`
|
||||
}
|
||||
if err := json.Unmarshal(item, &message); err != nil {
|
||||
return nil, false, fmt.Errorf("decode Codex agent message: %w", err)
|
||||
}
|
||||
if message.Author == "" || message.Recipient == "" || len(message.Content) == 0 {
|
||||
return nil, false, fmt.Errorf("Codex agent message requires author, recipient, and content")
|
||||
}
|
||||
parts := make([]json.RawMessage, 0, len(message.Content))
|
||||
for _, raw := range message.Content {
|
||||
var content struct {
|
||||
Type string `json:"type"`
|
||||
Text *string `json:"text"`
|
||||
EncryptedContent *string `json:"encrypted_content"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &content); err != nil {
|
||||
return nil, false, fmt.Errorf("decode Codex agent message content: %w", err)
|
||||
}
|
||||
switch content.Type {
|
||||
case "input_text":
|
||||
if content.Text == nil {
|
||||
return nil, false, fmt.Errorf("Codex agent message input_text requires text")
|
||||
}
|
||||
parts = append(parts, raw)
|
||||
case "encrypted_content":
|
||||
// Codex labels these payloads encrypted even when they are plain text.
|
||||
if content.EncryptedContent == nil {
|
||||
return nil, false, fmt.Errorf("Codex agent message encrypted_content requires a value")
|
||||
}
|
||||
text, err := json.Marshal(map[string]string{
|
||||
"type": "input_text",
|
||||
"text": *content.EncryptedContent,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("encode Codex agent message text: %w", err)
|
||||
}
|
||||
parts = append(parts, text)
|
||||
default:
|
||||
return nil, false, fmt.Errorf("unsupported Codex agent message content type %q", content.Type)
|
||||
}
|
||||
}
|
||||
|
||||
// Keep the envelope in sync with openai.AgentMessageEnvelopeFormat.
|
||||
envelope, err := json.Marshal(map[string]string{
|
||||
"type": "input_text",
|
||||
"text": fmt.Sprintf("Agent message from %q to %q:\n", message.Author, message.Recipient),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("encode Codex agent message envelope: %w", err)
|
||||
}
|
||||
var converted map[string]json.RawMessage
|
||||
if err := json.Unmarshal(item, &converted); err != nil {
|
||||
return nil, false, fmt.Errorf("decode Codex agent message: %w", err)
|
||||
}
|
||||
converted["type"] = json.RawMessage(`"message"`)
|
||||
converted["role"] = json.RawMessage(`"user"`)
|
||||
delete(converted, "author")
|
||||
delete(converted, "recipient")
|
||||
converted["content"], err = json.Marshal(append([]json.RawMessage{envelope}, parts...))
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("encode Codex agent message content: %w", err)
|
||||
}
|
||||
body, err := json.Marshal(converted)
|
||||
return body, true, err
|
||||
case "function_call", "function_call_output":
|
||||
return item, true, nil
|
||||
case "tool_search_call", "tool_search_output", "compaction_trigger":
|
||||
|
||||
@@ -54,6 +54,15 @@ type ResponsesFileContent struct {
|
||||
|
||||
func (ResponsesFileContent) responsesContent() {}
|
||||
|
||||
// ResponsesEncryptedContent is content a provider labeled as encrypted; for
|
||||
// Ollama-native conversations the value is plain text, which we accept as-is.
|
||||
type ResponsesEncryptedContent struct {
|
||||
Type string `json:"type"` // always "encrypted_content"
|
||||
EncryptedContent string `json:"encrypted_content"`
|
||||
}
|
||||
|
||||
func (ResponsesEncryptedContent) responsesContent() {}
|
||||
|
||||
type ResponsesInputMessage struct {
|
||||
Type string `json:"type"` // always "message"
|
||||
Role string `json:"role"` // one of `user`, `system`, `developer`
|
||||
@@ -139,6 +148,12 @@ func unmarshalResponsesContent(data []byte) (ResponsesContent, error) {
|
||||
return nil, err
|
||||
}
|
||||
return content, nil
|
||||
case "encrypted_content":
|
||||
var content ResponsesEncryptedContent
|
||||
if err := json.Unmarshal(data, &content); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return content, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown content type: %s", typeField.Type)
|
||||
}
|
||||
@@ -264,6 +279,55 @@ type ResponsesReasoningInput struct {
|
||||
|
||||
func (ResponsesReasoningInput) responsesInputItem() {}
|
||||
|
||||
// AgentMessageEnvelopeFormat is the routing prefix for converted agent
|
||||
// messages; internal/proxy uses the same text (cross-pinned by tests).
|
||||
const AgentMessageEnvelopeFormat = "Agent message from %q to %q:\n"
|
||||
|
||||
func agentMessageContent(author, recipient, content string) string {
|
||||
if author == "" && recipient == "" {
|
||||
return content
|
||||
}
|
||||
return fmt.Sprintf(AgentMessageEnvelopeFormat+"%s", author, recipient, content)
|
||||
}
|
||||
|
||||
// ResponsesAgentMessageInput is a message passed between Codex agents in the
|
||||
// multi-agent collaboration flow.
|
||||
type ResponsesAgentMessageInput struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
Type string `json:"type"` // always "agent_message"
|
||||
Author string `json:"author"`
|
||||
Recipient string `json:"recipient"`
|
||||
Content []ResponsesContent `json:"content"`
|
||||
}
|
||||
|
||||
func (ResponsesAgentMessageInput) responsesInputItem() {}
|
||||
|
||||
func (m *ResponsesAgentMessageInput) UnmarshalJSON(data []byte) error {
|
||||
var aux struct {
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Author string `json:"author"`
|
||||
Recipient string `json:"recipient"`
|
||||
Content []json.RawMessage `json:"content"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &aux); err != nil {
|
||||
return err
|
||||
}
|
||||
m.ID = aux.ID
|
||||
m.Type = aux.Type
|
||||
m.Author = aux.Author
|
||||
m.Recipient = aux.Recipient
|
||||
m.Content = make([]ResponsesContent, 0, len(aux.Content))
|
||||
for i, raw := range aux.Content {
|
||||
content, err := unmarshalResponsesContent(raw)
|
||||
if err != nil {
|
||||
return fmt.Errorf("content[%d]: %w", i, err)
|
||||
}
|
||||
m.Content = append(m.Content, content)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// unmarshalResponsesInputItem unmarshals a single input item from JSON.
|
||||
func unmarshalResponsesInputItem(data []byte) (ResponsesInputItem, error) {
|
||||
var typeField struct {
|
||||
@@ -324,6 +388,12 @@ func unmarshalResponsesInputItem(data []byte) (ResponsesInputItem, error) {
|
||||
return nil, err
|
||||
}
|
||||
return call, nil
|
||||
case "agent_message":
|
||||
var agentMessage ResponsesAgentMessageInput
|
||||
if err := json.Unmarshal(data, &agentMessage); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return agentMessage, nil
|
||||
case "compaction":
|
||||
var compaction ResponsesCompactionItem
|
||||
if err := json.Unmarshal(data, &compaction); err != nil {
|
||||
@@ -498,6 +568,15 @@ func FromResponsesRequest(r ResponsesRequest) (*api.ChatRequest, error) {
|
||||
case ResponsesReasoningInput:
|
||||
// Store thinking to merge with the next assistant message
|
||||
pendingThinking = v.EncryptedContent
|
||||
case ResponsesAgentMessageInput:
|
||||
content, _, err := convertResponsesContent(v.Content)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
messages = append(messages, api.Message{
|
||||
Role: "user",
|
||||
Content: agentMessageContent(v.Author, v.Recipient, content),
|
||||
})
|
||||
case ResponsesInputMessage:
|
||||
msg, err := convertInputMessage(v)
|
||||
if err != nil {
|
||||
@@ -956,6 +1035,8 @@ func convertResponsesContent(contents []ResponsesContent) (string, []api.ImageDa
|
||||
content += v.Text
|
||||
case ResponsesOutputTextContent:
|
||||
content += v.Text
|
||||
case ResponsesEncryptedContent:
|
||||
content += v.EncryptedContent
|
||||
case ResponsesImageContent:
|
||||
if v.ImageURL == "" {
|
||||
continue // Skip if no URL (FileID not supported)
|
||||
|
||||
@@ -531,6 +531,15 @@ func compactionMessage(item ResponsesInputItem) (api.Message, string, error) {
|
||||
thinking = "[opaque reasoning state omitted during Ollama compaction]"
|
||||
}
|
||||
return api.Message{Role: "assistant", Thinking: thinking}, "reasoning", nil
|
||||
case ResponsesAgentMessageInput:
|
||||
content, _, err := convertResponsesContent(value.Content)
|
||||
if err != nil {
|
||||
return api.Message{}, "", err
|
||||
}
|
||||
return api.Message{
|
||||
Role: "user",
|
||||
Content: agentMessageContent(value.Author, value.Recipient, content),
|
||||
}, "message", nil
|
||||
case ResponsesCompactionItem, ResponsesCompactionTrigger:
|
||||
return api.Message{}, "", errors.New("unexpected compaction control item")
|
||||
default:
|
||||
|
||||
@@ -3,6 +3,7 @@ package openai
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -790,3 +791,29 @@ func TestCompactionStreamContainsExactlyOneCompletedItem(t *testing.T) {
|
||||
t.Fatalf("done=%d completed=%d events=%+v", done, completed, events)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompactionMessageAgentMessage(t *testing.T) {
|
||||
var item ResponsesAgentMessageInput
|
||||
err := json.Unmarshal([]byte(`{
|
||||
"type": "agent_message",
|
||||
"author": "/root",
|
||||
"recipient": "/root/worker",
|
||||
"content": [
|
||||
{"type": "input_text", "text": "Message Type: NEW_TASK\nPayload:\n"},
|
||||
{"type": "encrypted_content", "encrypted_content": "analyze the parser"}
|
||||
]
|
||||
}`), &item)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
msg, kind, err := compactionMessage(item)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := AgentMessageEnvelopeFormat + "Message Type: NEW_TASK\nPayload:\nanalyze the parser"
|
||||
want = fmt.Sprintf(want, "/root", "/root/worker")
|
||||
if kind != "message" || msg.Role != "user" || msg.Content != want {
|
||||
t.Fatalf("kind=%q msg=%#v", kind, msg)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -392,6 +392,41 @@ func TestFromResponsesRequestMergesMessageAfterFunctionCall(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestFromResponsesRequestAgentMessage(t *testing.T) {
|
||||
var req ResponsesRequest
|
||||
err := json.Unmarshal([]byte(`{
|
||||
"model": "test-model",
|
||||
"input": [
|
||||
{"role": "user", "content": "Delegate the parser analysis."},
|
||||
{"type": "function_call", "call_id": "call_spawn", "namespace": "collaboration", "name": "spawn_agent", "arguments": "{\"task_name\":\"worker\"}"},
|
||||
{"type": "function_call_output", "call_id": "call_spawn", "output": "{\"task_name\":\"/root/worker\"}"},
|
||||
{"type": "agent_message", "id": "amsg_1", "author": "/root", "recipient": "/root/worker", "content": [
|
||||
{"type": "input_text", "text": "Message Type: NEW_TASK\nTask name: /root/worker\nSender: /root\nPayload:\n"},
|
||||
{"type": "encrypted_content", "encrypted_content": "Analyze the parser module and report issues."}
|
||||
]}
|
||||
]
|
||||
}`), &req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
chat, err := FromResponsesRequest(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(chat.Messages) != 4 {
|
||||
t.Fatalf("messages = %#v", chat.Messages)
|
||||
}
|
||||
|
||||
agentMsg := chat.Messages[3]
|
||||
want := "Agent message from \"/root\" to \"/root/worker\":\n" +
|
||||
"Message Type: NEW_TASK\nTask name: /root/worker\nSender: /root\nPayload:\n" +
|
||||
"Analyze the parser module and report issues."
|
||||
if agentMsg.Role != "user" || agentMsg.Content != want {
|
||||
t.Fatalf("agent message = %#v", agentMsg)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResponsesRequest_UnmarshalJSON(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -3054,3 +3089,50 @@ func TestResponsesStreamConverter_FinalOutputKeepsStreamedItemOrder(t *testing.T
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFromResponsesRequestAgentMessageWithoutRouting(t *testing.T) {
|
||||
var req ResponsesRequest
|
||||
err := json.Unmarshal([]byte(`{
|
||||
"model": "test-model",
|
||||
"input": [{"type": "agent_message", "content": [
|
||||
{"type": "encrypted_content", "encrypted_content": "bare payload"}
|
||||
]}]
|
||||
}`), &req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
chat, err := FromResponsesRequest(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(chat.Messages) != 1 {
|
||||
t.Fatalf("messages = %#v", chat.Messages)
|
||||
}
|
||||
// No routing fields: payload passes through without an envelope.
|
||||
if chat.Messages[0].Role != "user" || chat.Messages[0].Content != "bare payload" {
|
||||
t.Fatalf("agent message = %#v", chat.Messages[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestFromResponsesRequestAcceptsEncryptedContentPart(t *testing.T) {
|
||||
var req ResponsesRequest
|
||||
err := json.Unmarshal([]byte(`{
|
||||
"model": "test-model",
|
||||
"input": [{"type": "message", "role": "user", "content": [
|
||||
{"type": "input_text", "text": "prefix "},
|
||||
{"type": "encrypted_content", "encrypted_content": "opaque to us"}
|
||||
]}]
|
||||
}`), &req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
chat, err := FromResponsesRequest(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(chat.Messages) != 1 || chat.Messages[0].Content != "prefix opaque to us" {
|
||||
t.Fatalf("messages = %#v", chat.Messages)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user