feat(session,llm): restore session runtime on resume; align Responses API details
This commit is contained in:
+22
-1
@@ -124,6 +124,9 @@ func buildResponsesRequest(req ChatRequest) map[string]any {
|
||||
"input": buildResponsesInput(req.Messages),
|
||||
"stream": true,
|
||||
}
|
||||
if instructions := responsesInstructions(req.Messages); instructions != "" {
|
||||
payload["instructions"] = instructions
|
||||
}
|
||||
if len(req.Tools) > 0 {
|
||||
payload["tools"] = buildResponsesTools(req.Tools)
|
||||
}
|
||||
@@ -153,7 +156,9 @@ func buildResponsesInput(messages []Message) []any {
|
||||
for _, msg := range messages {
|
||||
switch msg.Role {
|
||||
case RoleSystem:
|
||||
items = append(items, responsesMessageInput{Role: "system", Content: msg.Content})
|
||||
// System instructions are sent as the top-level "instructions"
|
||||
// field and must not be duplicated inside the input array.
|
||||
continue
|
||||
case RoleUser:
|
||||
items = append(items, responsesMessageInput{Role: "user", Content: msg.Content})
|
||||
case RoleAssistant:
|
||||
@@ -179,6 +184,22 @@ func buildResponsesInput(messages []Message) []any {
|
||||
return items
|
||||
}
|
||||
|
||||
// responsesInstructions extracts system messages into a single instructions
|
||||
// string, which is the OpenAI Responses API way to pass developer/system
|
||||
// guidance (and the most prompt-cache friendly).
|
||||
func responsesInstructions(messages []Message) string {
|
||||
var instructions []string
|
||||
for _, msg := range messages {
|
||||
if msg.Role != RoleSystem {
|
||||
continue
|
||||
}
|
||||
if text := strings.TrimSpace(msg.Content); text != "" {
|
||||
instructions = append(instructions, text)
|
||||
}
|
||||
}
|
||||
return strings.Join(instructions, "\n\n")
|
||||
}
|
||||
|
||||
func buildResponsesTools(tools []Tool) []responsesTool {
|
||||
out := make([]responsesTool, 0, len(tools))
|
||||
for _, tool := range tools {
|
||||
|
||||
@@ -59,12 +59,15 @@ func TestBuildResponsesRequest(t *testing.T) {
|
||||
if raw["model"] != "gpt-test" || raw["stream"] != true {
|
||||
t.Fatalf("model/stream = %#v / %#v", raw["model"], raw["stream"])
|
||||
}
|
||||
if raw["instructions"] != "be concise" {
|
||||
t.Fatalf("instructions = %#v", raw["instructions"])
|
||||
}
|
||||
if raw["tool_choice"] != "auto" {
|
||||
t.Fatalf("tool_choice = %#v", raw["tool_choice"])
|
||||
}
|
||||
|
||||
input, ok := raw["input"].([]any)
|
||||
if !ok || len(input) != 6 {
|
||||
if !ok || len(input) != 5 {
|
||||
t.Fatalf("input = %#v", raw["input"])
|
||||
}
|
||||
checkInputItem := func(index int, wantType, wantKey, wantValue string) {
|
||||
@@ -80,16 +83,15 @@ func TestBuildResponsesRequest(t *testing.T) {
|
||||
t.Fatalf("input[%d].type = %#v, want %q", index, item["type"], wantType)
|
||||
}
|
||||
}
|
||||
checkInputItem(0, "", "role", "system")
|
||||
checkInputItem(1, "", "role", "user")
|
||||
checkInputItem(2, "", "role", "assistant")
|
||||
checkInputItem(3, "function_call", "call_id", "call_1")
|
||||
checkInputItem(4, "function_call_output", "call_id", "call_1")
|
||||
checkInputItem(5, "", "role", "assistant")
|
||||
if item := input[3].(map[string]any); item["name"] != "file_read" || item["arguments"] != `{"path":"README.md"}` {
|
||||
checkInputItem(0, "", "role", "user")
|
||||
checkInputItem(1, "", "role", "assistant")
|
||||
checkInputItem(2, "function_call", "call_id", "call_1")
|
||||
checkInputItem(3, "function_call_output", "call_id", "call_1")
|
||||
checkInputItem(4, "", "role", "assistant")
|
||||
if item := input[2].(map[string]any); item["name"] != "file_read" || item["arguments"] != `{"path":"README.md"}` {
|
||||
t.Fatalf("function_call item = %#v", item)
|
||||
}
|
||||
if item := input[4].(map[string]any); item["output"] != "contents" {
|
||||
if item := input[3].(map[string]any); item["output"] != "contents" {
|
||||
t.Fatalf("function_call_output item = %#v", item)
|
||||
}
|
||||
|
||||
@@ -117,6 +119,36 @@ func TestBuildResponsesRequest(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestResponsesInstructionsCombinesSystemMessages(t *testing.T) {
|
||||
payload := buildResponsesRequest(ChatRequest{
|
||||
Model: "gpt-test",
|
||||
Messages: []Message{
|
||||
{Role: RoleSystem, Content: "first instruction"},
|
||||
{Role: RoleUser, Content: "hi"},
|
||||
{Role: RoleSystem, Content: "second instruction"},
|
||||
},
|
||||
})
|
||||
data, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var raw map[string]any
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if raw["instructions"] != "first instruction\n\nsecond instruction" {
|
||||
t.Fatalf("instructions = %#v", raw["instructions"])
|
||||
}
|
||||
input, ok := raw["input"].([]any)
|
||||
if !ok || len(input) != 1 {
|
||||
t.Fatalf("input = %#v", raw["input"])
|
||||
}
|
||||
item, ok := input[0].(map[string]any)
|
||||
if !ok || item["role"] != "user" {
|
||||
t.Fatalf("input[0] = %#v", input[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpenAIResponsesClientStreamsContentAndToolCalls(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/v1/responses" {
|
||||
|
||||
Reference in New Issue
Block a user