iop/apps/edge/internal/openai/hot_path_chat_gate_test.go
toki 495996fee4 feat(openai): 핫패스 에이전트 실행 경로를 확장한다
Anthropic·Chat 게이트와 관찰·종료 제어를 통합하고 관련 계약·검증 산출물을 반영한다.
2026-08-06 00:09:24 +09:00

793 lines
37 KiB
Go

package openai
import (
"bufio"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
"strings"
"sync"
"testing"
"time"
edgeservice "iop/apps/edge/internal/service"
"iop/packages/go/config"
iop "iop/proto/gen/iop"
)
func TestHotPathChatDirectStreamCodec(t *testing.T) {
candidate := anthropicTestCandidate(t, "openai")
providerStream := strings.Join([]string{
`data: {"id":"chatcmpl-chat-gate","object":"chat.completion.chunk","created":1777001001,"choices":[{"index":0,"delta":{"content":"alpha "},"finish_reason":null}]}`,
`data: {"id":"chatcmpl-chat-gate","object":"chat.completion.chunk","created":1777001001,"choices":[{"index":0,"delta":{"reasoning_content":"think "},"finish_reason":null}]}`,
`data: {"id":"chatcmpl-chat-gate","object":"chat.completion.chunk","created":1777001001,"choices":[{"index":0,"delta":{"content":"omega"},"finish_reason":null}]}`,
`data: {"id":"chatcmpl-chat-gate","object":"chat.completion.chunk","created":1777001001,"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"id":"provider-chat-gate","type":"function","function":{"name":"read_file","arguments":"{\"path\":"}}]},"finish_reason":null}]}`,
`data: {"id":"chatcmpl-chat-gate","object":"chat.completion.chunk","created":1777001001,"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\"README.md\"}"}}]},"finish_reason":null}]}`,
`data: {"id":"chatcmpl-chat-gate","object":"chat.completion.chunk","created":1777001001,"choices":[{"index":0,"delta":{},"finish_reason":"tool_calls"}],"usage":{"prompt_tokens":9,"completion_tokens":7,"total_tokens":16}}`,
`data: [DONE]`, "",
}, "\n\n")
srv, fake := newHotPathHandlerServer(t, candidate, staticProviderTunnelFrames(providerStream))
body := `{"model":"virtual-model","messages":[{"role":"user","content":"hello"}],"tools":[{"type":"function","function":{"name":"read_file","parameters":{"type":"object"}}}],"max_completion_tokens":64,"stream":true}`
response := serveHotPathChatBody(t, srv, body)
if response.Code != http.StatusOK {
t.Fatalf("status=%d body=%s", response.Code, response.Body.String())
}
chunks, done := decodeHotPathChatSSE(t, response.Body.String())
if done != 1 {
t.Fatalf("DONE count=%d body=%s", done, response.Body.String())
}
assertHotPathChatChunks(t, chunks, hotPathChatChunkExpectation{
ResponseID: "chatcmpl-chat-gate", Model: "virtual-model", Content: "alpha omega", Reasoning: "think ",
Kinds: []string{"content", "reasoning", "content", "tool", "tool", "terminal"},
ToolID: "chatcmpl-chat-gate-tool-1", ToolName: "read_file", ToolArgs: `{"path":"README.md"}`,
FinishReason: "tool_calls", PromptTokens: 9, CompletionTokens: 7,
})
if fake.poolSubmitCountSnapshot() != 1 {
t.Fatalf("selector submissions=%d, want 1", fake.poolSubmitCountSnapshot())
}
assertHotPathWaiting(t, srv, "chatcmpl-chat-gate-tool-1", "provider-chat-gate")
}
func TestHotPathChatToolIndexesAreMonotonic(t *testing.T) {
candidate := anthropicTestCandidate(t, "openai")
providerStream := strings.Join([]string{
`data: {"id":"chatcmpl-chat-tools","created":1777001004,"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"id":"provider-tool-0","type":"function","function":{"name":"read_file","arguments":"{\"path\":"}}]},"finish_reason":null}]}`,
`data: {"id":"chatcmpl-chat-tools","created":1777001004,"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\"a\"}"}}]},"finish_reason":null}]}`,
`data: {"id":"chatcmpl-chat-tools","created":1777001004,"choices":[{"index":0,"delta":{"tool_calls":[{"index":1,"id":"provider-tool-1","type":"function","function":{"name":"read_file","arguments":"{\"path\":"}}]},"finish_reason":null}]}`,
`data: {"id":"chatcmpl-chat-tools","created":1777001004,"choices":[{"index":0,"delta":{"tool_calls":[{"index":1,"function":{"arguments":"\"b\"}"}}]},"finish_reason":null}]}`,
`data: {"id":"chatcmpl-chat-tools","created":1777001004,"choices":[{"index":0,"delta":{},"finish_reason":"tool_calls"}]}`,
`data: [DONE]`, "",
}, "\n\n")
srv, _ := newHotPathHandlerServer(t, candidate, staticProviderTunnelFrames(providerStream))
response := serveHotPathChatBody(t, srv, `{"model":"virtual-model","messages":[{"role":"user","content":"tools"}],"tools":[{"type":"function","function":{"name":"read_file","parameters":{"type":"object"}}}],"stream":true}`)
chunks, done := decodeHotPathChatSSE(t, response.Body.String())
if response.Code != http.StatusOK || done != 1 {
t.Fatalf("status=%d DONE=%d body=%s", response.Code, done, response.Body.String())
}
var indexes []int
var ids []string
for _, chunk := range chunks {
choice := chunk["choices"].([]any)[0].(map[string]any)
delta := choice["delta"].(map[string]any)
tools, ok := delta["tool_calls"].([]any)
if !ok {
continue
}
tool := tools[0].(map[string]any)
indexes = append(indexes, int(tool["index"].(float64)))
if id, _ := tool["id"].(string); id != "" {
ids = append(ids, id)
}
}
if fmt.Sprint(indexes) != "[0 0 1 1]" || fmt.Sprint(ids) != "[chatcmpl-chat-tools-tool-1 chatcmpl-chat-tools-tool-2]" {
t.Fatalf("tool index/id sequence: indexes=%v ids=%v body=%s", indexes, ids, response.Body.String())
}
}
func TestHotPathChatCallerCapAndNonStream(t *testing.T) {
for _, capField := range []string{"max_tokens", "max_completion_tokens"} {
capField := capField
t.Run(capField+" preserves provider terminal", func(t *testing.T) {
candidate := anthropicTestCandidate(t, "openai")
providerStream := strings.Join([]string{
`data: {"id":"chatcmpl-chat-cap","created":1777001002,"choices":[{"index":0,"delta":{"content":"abcdefghij"},"finish_reason":null}]}`,
`data: {"id":"chatcmpl-chat-cap","created":1777001002,"choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}`,
`data: [DONE]`, "",
}, "\n\n")
srv, _ := newHotPathHandlerServer(t, candidate, staticProviderTunnelFrames(providerStream))
body := fmt.Sprintf(`{"model":"virtual-model","messages":[{"role":"user","content":"cap"}],%q:2,"stream":true}`, capField)
response := serveHotPathChatBody(t, srv, body)
chunks, done := decodeHotPathChatSSE(t, response.Body.String())
if response.Code != http.StatusOK || done != 1 {
t.Fatalf("status=%d DONE=%d body=%s", response.Code, done, response.Body.String())
}
assertHotPathChatChunks(t, chunks, hotPathChatChunkExpectation{
ResponseID: "chatcmpl-chat-cap", Model: "virtual-model", Content: "abcdefghij",
Kinds: []string{"content", "terminal"}, FinishReason: "stop",
})
assertHotPathTerminal(t, srv)
})
}
t.Run("non-stream compatibility", func(t *testing.T) {
candidate := anthropicTestCandidate(t, "openai")
providerBody := `{"id":"chatcmpl-chat-json","object":"chat.completion","created":1777001003,"model":"served-selector","choices":[{"index":0,"message":{"role":"assistant","content":"json final","reasoning_content":"json thought"},"finish_reason":"stop"}],"usage":{"prompt_tokens":4,"completion_tokens":3,"total_tokens":7}}`
srv, fake := newHotPathHandlerServer(t, candidate, staticProviderTunnelFrames(providerBody))
response := serveHotPathChatBody(t, srv, `{"model":"virtual-model","messages":[{"role":"user","content":"json"}],"stream":false}`)
if response.Code != http.StatusOK {
t.Fatalf("status=%d body=%s", response.Code, response.Body.String())
}
var decoded struct {
ID string `json:"id"`
Model string `json:"model"`
Choices []struct {
Message chatMessage `json:"message"`
FinishReason string `json:"finish_reason"`
} `json:"choices"`
Usage openAIUsage `json:"usage"`
}
if err := json.Unmarshal(response.Body.Bytes(), &decoded); err != nil {
t.Fatal(err)
}
if decoded.ID != "chatcmpl-chat-json" || decoded.Model != "virtual-model" || len(decoded.Choices) != 1 ||
decoded.Choices[0].Message.Content != "json final" || decoded.Choices[0].Message.ReasoningContent != "json thought" ||
decoded.Choices[0].FinishReason != "stop" || decoded.Usage.PromptTokens != 4 || decoded.Usage.CompletionTokens != 3 {
t.Fatalf("non-stream response mismatch: %+v", decoded)
}
if fake.poolSubmitCountSnapshot() != 1 {
t.Fatalf("selector submissions=%d, want 1", fake.poolSubmitCountSnapshot())
}
assertHotPathTerminal(t, srv)
})
}
func TestHotPathChatMixedProviderStages(t *testing.T) {
decodedReview, err := decodeAnthropicPresetSSE([]byte(hotPathChatMixedReviewSSE("req-decode-check")))
if err != nil || len(decodedReview.ToolCalls) != 1 || len(decodedReview.Deltas) != 3 {
t.Fatalf("mixed review fixture decode: tools=%d deltas=%d err=%v output=%+v", len(decodedReview.ToolCalls), len(decodedReview.Deltas), err, decodedReview)
}
openAICandidate := anthropicTestCandidate(t, "openai")
anthropicCandidate := anthropicTestCandidate(t, "anthropic")
service := &hotPathChatGateScriptedService{}
service.steps = []hotPathChatGateStep{
{candidate: openAICandidate, body: func(requestID string) string { return scriptedArtifactPrepare("openai", requestID) }},
{candidate: openAICandidate, body: func(requestID string) string { return scriptedArtifactPair("openai", requestID) }},
{candidate: openAICandidate, body: func(requestID string) string { return scriptedArtifactLocalRead("openai", requestID) }},
{candidate: openAICandidate, contentType: "text/event-stream", body: func(string) string { return hotPathChatMixedLocalSSE() }},
{candidate: anthropicCandidate, contentType: "text/event-stream", body: hotPathChatMixedReviewSSE},
}
preset := hotPathSelectorPreset([]string{config.ModeDirect, config.ModeLight})
preset.WorkspaceTools = []config.ExecutionWorkspaceToolAlternative{scriptedLightWorkspaceAlternative()}
srv := NewServer(config.EdgeOpenAIConf{}, service, nil)
srv.SetEdgeID("edge-chat-gate-mixed")
srv.SetExecutionPresets([]config.ExecutionPreset{preset})
providers := map[string]string{
openAICandidate.ProviderID: "served-openai", anthropicCandidate.ProviderID: "served-anthropic",
}
srv.SetModelCatalog([]config.ModelCatalogEntry{
{ID: "virtual-model", ExecutionPreset: preset.ID},
{ID: "selector-model", Providers: providers},
{ID: "local-model", Providers: providers},
{ID: "review-model", Providers: providers},
})
tools := scriptedLightTools("openai")
history := []any{map[string]any{"role": "user", "content": "mixed provider task"}}
consume := func(response *httptest.ResponseRecorder, results []string) {
t.Helper()
assistant, ids, err := artifactAssistantFromResponse("openai", response.Body.Bytes())
if err != nil || len(ids) != len(results) {
t.Fatalf("consume tool response: ids=%v err=%v body=%s", ids, err, response.Body.String())
}
history = append(history, assistant)
history = scriptedArtifactAppendResults("openai", history, ids, results)
}
request := func(stream bool) *httptest.ResponseRecorder {
t.Helper()
body := scriptedArtifactRequestBodyWithOptions(t, "openai", tools, history, 64, stream)
return serveScriptedArtifactRequest(t, srv, "openai", body)
}
consume(request(false), []string{`{"written":true}`})
consume(request(false), []string{`{"written":true}`, `{"written":true}`})
consume(request(false), []string{`{"written":true}`})
response := request(true)
if response.Code != http.StatusOK {
t.Fatalf("status=%d body=%s", response.Code, response.Body.String())
}
chunks, done := decodeHotPathChatSSE(t, response.Body.String())
if done != 1 {
t.Fatalf("DONE count=%d body=%s", done, response.Body.String())
}
requestID, snapshot := soleHotPathSnapshot(t, srv)
assertHotPathChatChunks(t, chunks, hotPathChatChunkExpectation{
ResponseID: "chatcmpl-mixed-local", Model: "virtual-model", Content: "local-A local-Breview-visible", Reasoning: "local-think review-think ",
Kinds: []string{"content", "reasoning", "content", "reasoning", "content", "tool", "terminal"},
ToolName: "write_file", ToolArgs: hotPathChatReviewArguments(requestID),
FinishReason: "tool_calls", PromptTokens: 12, CompletionTokens: 7,
})
for _, chunk := range chunks {
if chunk["id"] == requestID {
t.Fatalf("logical request identity became the public response id: %+v", chunk)
}
}
for _, internalID := range []string{snapshot.ActiveStageID, "run-chat-gate-4", "run-chat-gate-5", "msg-mixed-review"} {
if strings.Contains(response.Body.String(), internalID) {
t.Fatalf("internal or later-stage identity %q leaked: %s", internalID, response.Body.String())
}
}
if got := service.requestCount(); got != 5 {
t.Fatalf("provider submissions=%d, want 5", got)
}
}
func TestHotPathChatProviderErrorBeforeCommit(t *testing.T) {
candidate := anthropicTestCandidate(t, "openai")
frames := make(chan *iop.ProviderTunnelFrame, 2)
frames <- &iop.ProviderTunnelFrame{
Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_RESPONSE_START, StatusCode: http.StatusBadGateway,
}
frames <- &iop.ProviderTunnelFrame{Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_END, End: true}
close(frames)
srv, fake := newHotPathHandlerServer(t, candidate, frames)
response := serveHotPathChatBody(t, srv, `{"model":"virtual-model","messages":[{"role":"user","content":"fail"}],"stream":true}`)
if response.Code != http.StatusBadGateway || !strings.Contains(response.Body.String(), `"type":"run_error"`) || strings.Contains(response.Body.String(), "[DONE]") {
t.Fatalf("pre-commit error mismatch: status=%d body=%s", response.Code, response.Body.String())
}
if fake.poolSubmitCountSnapshot() != 1 {
t.Fatalf("selector submissions=%d, want 1", fake.poolSubmitCountSnapshot())
}
assertHotPathTerminal(t, srv)
}
func TestHotPathChatFlushesVisibleDeltaBeforeProviderTerminal(t *testing.T) {
candidate := anthropicTestCandidate(t, "openai")
frames := make(chan *iop.ProviderTunnelFrame, 4)
frames <- &iop.ProviderTunnelFrame{
Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_RESPONSE_START,
StatusCode: http.StatusOK, Headers: map[string]string{"Content-Type": "text/event-stream"},
RunId: "run-chat-gate-4",
}
frames <- &iop.ProviderTunnelFrame{
Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_BODY,
Body: []byte("data: {\"id\":\"chatcmpl-live-local\",\"created\":1777001201,\"choices\":[{\"index\":0,\"delta\":{\"content\":\"early-visible\"},\"finish_reason\":null}]}\n\n"),
RunId: "run-chat-gate-4",
}
service := &hotPathChatGateScriptedService{}
service.steps = []hotPathChatGateStep{
{candidate: candidate, body: func(requestID string) string { return scriptedArtifactPrepare("openai", requestID) }},
{candidate: candidate, body: func(requestID string) string { return scriptedArtifactPair("openai", requestID) }},
{candidate: candidate, body: func(requestID string) string { return scriptedArtifactLocalRead("openai", requestID) }},
{candidate: candidate, contentType: "text/event-stream", frames: frames},
}
preset := hotPathSelectorPreset([]string{config.ModeDirect, config.ModeLight})
preset.WorkspaceTools = []config.ExecutionWorkspaceToolAlternative{scriptedLightWorkspaceAlternative()}
srv := NewServer(config.EdgeOpenAIConf{}, service, nil)
srv.SetEdgeID("edge-chat-gate-live")
srv.SetExecutionPresets([]config.ExecutionPreset{preset})
providers := map[string]string{candidate.ProviderID: "served-openai"}
srv.SetModelCatalog([]config.ModelCatalogEntry{
{ID: "virtual-model", ExecutionPreset: preset.ID},
{ID: "selector-model", Providers: providers},
{ID: "local-model", Providers: providers},
{ID: "review-model", Providers: providers},
})
tools := scriptedLightTools("openai")
history := []any{map[string]any{"role": "user", "content": "flush before terminal"}}
consume := func(response *httptest.ResponseRecorder, results []string) {
t.Helper()
assistant, ids, err := artifactAssistantFromResponse("openai", response.Body.Bytes())
if err != nil || len(ids) != len(results) {
t.Fatalf("consume setup response: ids=%v err=%v body=%s", ids, err, response.Body.String())
}
history = append(history, assistant)
history = scriptedArtifactAppendResults("openai", history, ids, results)
}
requestSetup := func() *httptest.ResponseRecorder {
body := scriptedArtifactRequestBodyWithOptions(t, "openai", tools, history, 64, false)
return serveScriptedArtifactRequest(t, srv, "openai", body)
}
consume(requestSetup(), []string{`{"written":true}`})
consume(requestSetup(), []string{`{"written":true}`, `{"written":true}`})
consume(requestSetup(), []string{`{"written":true}`})
httpServer := httptest.NewServer(srv.routes())
defer httpServer.Close()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
body := scriptedArtifactRequestBodyWithOptions(t, "openai", tools, history, 64, true)
request, err := http.NewRequestWithContext(ctx, http.MethodPost, httpServer.URL+"/v1/chat/completions", strings.NewReader(string(body)))
if err != nil {
t.Fatal(err)
}
response, err := http.DefaultClient.Do(request)
if err != nil {
t.Fatalf("stream request did not flush before terminal: %v", err)
}
defer response.Body.Close()
reader := bufio.NewReader(response.Body)
roleFrame, err := readHotPathSSEFrame(reader)
if err != nil {
t.Fatalf("read role before terminal: %v", err)
}
contentFrame, err := readHotPathSSEFrame(reader)
if err != nil {
t.Fatalf("read content before terminal: %v", err)
}
early := roleFrame + contentFrame
if response.StatusCode != http.StatusOK || !strings.Contains(early, `"role":"assistant"`) ||
!strings.Contains(early, `"content":"early-visible"`) || !strings.Contains(early, `"id":"chatcmpl-live-local"`) ||
strings.Contains(early, "[DONE]") || strings.Contains(early, `"finish_reason":"`) {
t.Fatalf("pre-terminal flush mismatch: status=%d body=%s", response.StatusCode, early)
}
requestID, snapshot := soleHotPathSnapshot(t, srv)
frames <- &iop.ProviderTunnelFrame{
Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_BODY,
Body: []byte("data: {\"id\":\"chatcmpl-live-local\",\"created\":1777001201,\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"id\":\"provider-live-tool\",\"type\":\"function\",\"function\":{\"name\":\"run_command\",\"arguments\":\"{\\\"command\\\":\\\"status\\\"}\"}}]},\"finish_reason\":null}]}\n\n"),
RunId: "run-chat-gate-4",
}
toolFrame, err := readHotPathSSEFrame(reader)
if err != nil {
t.Fatalf("read tool fragment before terminal: %v", err)
}
if !strings.Contains(toolFrame, `"tool_calls"`) || !strings.Contains(toolFrame, `"name":"run_command"`) ||
strings.Contains(toolFrame, "[DONE]") || strings.Contains(toolFrame, `"finish_reason":"`) {
t.Fatalf("pre-terminal tool flush mismatch: %s", toolFrame)
}
frames <- &iop.ProviderTunnelFrame{
Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_BODY,
Body: []byte("data: {\"id\":\"chatcmpl-live-local\",\"created\":1777001201,\"choices\":[{\"index\":0,\"delta\":{},\"finish_reason\":\"tool_calls\"}],\"usage\":{\"prompt_tokens\":3,\"completion_tokens\":1,\"total_tokens\":4}}\n\ndata: [DONE]\n\n"),
RunId: "run-chat-gate-4",
}
frames <- &iop.ProviderTunnelFrame{Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_END, End: true, RunId: "run-chat-gate-4"}
close(frames)
rest, err := io.ReadAll(reader)
if err != nil {
t.Fatalf("read terminal stream: %v", err)
}
wire := early + toolFrame + string(rest)
chunks, done := decodeHotPathChatSSE(t, wire)
if done != 1 {
t.Fatalf("DONE count=%d body=%s", done, wire)
}
assertHotPathChatChunks(t, chunks, hotPathChatChunkExpectation{
ResponseID: "chatcmpl-live-local", Model: "virtual-model", Content: "early-visible",
Kinds: []string{"content", "tool", "terminal"}, ToolName: "run_command", ToolArgs: `{"command":"status"}`,
FinishReason: "tool_calls", PromptTokens: 3, CompletionTokens: 1,
})
for _, internalID := range []string{requestID, snapshot.ActiveStageID, "run-chat-gate-4", "provider-live-tool"} {
if strings.Contains(wire, internalID) {
t.Fatalf("internal identity %q leaked: %s", internalID, wire)
}
}
}
func TestHotPathNormalizedStageSourceRequiresIdentityOnEveryVisibleAndCompleteEvent(t *testing.T) {
for _, eventType := range []string{"delta", "reasoning_delta", "complete"} {
eventType := eventType
t.Run(eventType, func(t *testing.T) {
source := &hotPathNormalizedStageSource{}
if err := source.observeRunEvent(&iop.RunEvent{
Type: "delta", Delta: "first", Metadata: map[string]string{hotPathOpenAIResponseIDMetadata: "chatcmpl-event-scoped"},
}); err != nil {
t.Fatalf("observe valid first event: %v", err)
}
if err := source.observeRunEvent(&iop.RunEvent{Type: eventType, Delta: "missing"}); err == nil {
t.Fatalf("%s without event-scoped identity was accepted", eventType)
}
})
}
}
func TestHotPathLiveStageTerminalReason(t *testing.T) {
tests := []struct {
name, protocol, want string
frames chan *iop.ProviderTunnelFrame
}{
{
name: "OpenAI length", protocol: "openai", want: "length",
frames: staticProviderTunnelFrames(strings.Join([]string{
`data: {"id":"chatcmpl-length-probe","choices":[{"delta":{"content":"limited"},"finish_reason":null}]}`,
`data: {"id":"chatcmpl-length-probe","choices":[{"delta":{},"finish_reason":"length"}]}`,
`data: [DONE]`, "",
}, "\n\n")),
},
{
name: "Anthropic max tokens", protocol: "anthropic", want: "max_tokens",
frames: anthropicTunnelFrames(http.StatusOK, "text/event-stream", []byte(strings.ReplaceAll(strings.Join([]string{
`event: message_start\ndata: {"type":"message_start","message":{"id":"msg-length-probe","usage":{"input_tokens":2}}}`,
`event: content_block_start\ndata: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":"limited"}}`,
`event: message_delta\ndata: {"type":"message_delta","delta":{"stop_reason":"max_tokens","stop_sequence":null},"usage":{"output_tokens":3}}`,
`event: message_stop\ndata: {"type":"message_stop"}`, "",
}, "\n\n"), `\n`, "\n"))),
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
source := newHotPathTunnelStageSource(
edgeservice.ProviderTunnelStream{Frames: test.frames}, time.Second, newHotPathStageDecoderForProtocol(test.protocol),
)
outer := newHotPathOuterTurn("")
output, terminal, err := runHotPathStreamingStage(
context.Background(), outer,
hotPathStageMeta{StageID: "terminal-reason", Protocol: test.protocol, Model: "model", Provider: "provider", AttemptID: test.name},
source, source, &hotPathCountingController{},
)
if err != nil {
t.Fatalf("run live stage: %v", err)
}
if !terminal.Success || terminal.Reason != test.want || output.TerminalReason != test.want || output.Content != "limited" {
t.Fatalf("terminal reason projection: terminal=%+v output=%+v", terminal, output)
}
})
}
t.Run("Normalized max tokens", func(t *testing.T) {
const responseID = "chatcmpl-normalized-length-probe"
source := newHotPathNormalizedStageSource(edgeservice.RunStream{Events: bufferedRunEvents(
&iop.RunEvent{Type: "delta", Delta: "limited", Metadata: map[string]string{hotPathOpenAIResponseIDMetadata: responseID}},
&iop.RunEvent{Type: "complete", Metadata: map[string]string{
hotPathOpenAIResponseIDMetadata: responseID, "finish_reason": "max_tokens",
}},
)}, time.Second)
outer := newHotPathOuterTurn("")
output, terminal, err := runHotPathStreamingStage(
context.Background(), outer,
hotPathStageMeta{StageID: "normalized-terminal-reason", Protocol: "openai", Model: "model", Provider: "provider", AttemptID: "normalized"},
source, source, &hotPathCountingController{},
)
if err != nil {
t.Fatalf("run normalized live stage: %v", err)
}
if !terminal.Success || terminal.Reason != "max_tokens" || output.TerminalReason != "max_tokens" || output.Content != "limited" {
t.Fatalf("normalized terminal reason projection: terminal=%+v output=%+v", terminal, output)
}
})
}
func TestHotPathChatProviderLengthFlushesBeforeTerminalAndStopsLight(t *testing.T) {
candidate := anthropicTestCandidate(t, "openai")
frames := make(chan *iop.ProviderTunnelFrame, 4)
frames <- &iop.ProviderTunnelFrame{
Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_RESPONSE_START, StatusCode: http.StatusOK,
Headers: map[string]string{"Content-Type": "text/event-stream"}, RunId: "run-chat-length-local",
}
frames <- &iop.ProviderTunnelFrame{
Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_BODY, RunId: "run-chat-length-local",
Body: []byte("data: {\"id\":\"chatcmpl-provider-length\",\"created\":1777001301,\"choices\":[{\"index\":0,\"delta\":{\"content\":\"provider-limited\"},\"finish_reason\":null}]}\n\n"),
}
service := &hotPathChatGateScriptedService{}
service.steps = []hotPathChatGateStep{
{candidate: candidate, body: func(requestID string) string { return scriptedArtifactPrepare("openai", requestID) }},
{candidate: candidate, body: func(requestID string) string { return scriptedArtifactPair("openai", requestID) }},
{candidate: candidate, body: func(requestID string) string { return scriptedArtifactLocalRead("openai", requestID) }},
{candidate: candidate, contentType: "text/event-stream", frames: frames},
}
preset := hotPathSelectorPreset([]string{config.ModeDirect, config.ModeLight})
preset.WorkspaceTools = []config.ExecutionWorkspaceToolAlternative{scriptedLightWorkspaceAlternative()}
srv := NewServer(config.EdgeOpenAIConf{}, service, nil)
srv.SetEdgeID("edge-chat-provider-length")
srv.SetExecutionPresets([]config.ExecutionPreset{preset})
providers := map[string]string{candidate.ProviderID: "served-openai"}
srv.SetModelCatalog([]config.ModelCatalogEntry{
{ID: "virtual-model", ExecutionPreset: preset.ID},
{ID: "selector-model", Providers: providers},
{ID: "local-model", Providers: providers},
{ID: "review-model", Providers: providers},
})
tools := scriptedLightTools("openai")
history := []any{map[string]any{"role": "user", "content": "provider length terminal"}}
consume := func(response *httptest.ResponseRecorder, results []string) {
t.Helper()
assistant, ids, err := artifactAssistantFromResponse("openai", response.Body.Bytes())
if err != nil || len(ids) != len(results) {
t.Fatalf("consume setup response: ids=%v err=%v body=%s", ids, err, response.Body.String())
}
history = append(history, assistant)
history = scriptedArtifactAppendResults("openai", history, ids, results)
}
requestSetup := func() *httptest.ResponseRecorder {
body := scriptedArtifactRequestBodyWithOptions(t, "openai", tools, history, 64, false)
return serveScriptedArtifactRequest(t, srv, "openai", body)
}
consume(requestSetup(), []string{`{"written":true}`})
consume(requestSetup(), []string{`{"written":true}`, `{"written":true}`})
consume(requestSetup(), []string{`{"written":true}`})
httpServer := httptest.NewServer(srv.routes())
defer httpServer.Close()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
body := scriptedArtifactRequestBodyWithOptions(t, "openai", tools, history, 64, true)
request, err := http.NewRequestWithContext(ctx, http.MethodPost, httpServer.URL+"/v1/chat/completions", strings.NewReader(string(body)))
if err != nil {
t.Fatal(err)
}
response, err := http.DefaultClient.Do(request)
if err != nil {
t.Fatalf("stream request did not flush before terminal: %v", err)
}
defer response.Body.Close()
reader := bufio.NewReader(response.Body)
roleFrame, err := readHotPathSSEFrame(reader)
if err != nil {
t.Fatalf("read role before provider terminal: %v", err)
}
contentFrame, err := readHotPathSSEFrame(reader)
if err != nil {
t.Fatalf("read content before provider terminal: %v", err)
}
early := roleFrame + contentFrame
if response.StatusCode != http.StatusOK || !strings.Contains(early, `"role":"assistant"`) ||
!strings.Contains(early, `"content":"provider-limited"`) || !strings.Contains(early, `"id":"chatcmpl-provider-length"`) ||
strings.Contains(early, "[DONE]") || strings.Contains(early, `"finish_reason":"`) {
t.Fatalf("pre-terminal provider length flush mismatch: status=%d body=%s", response.StatusCode, early)
}
requestID, snapshot := soleHotPathSnapshot(t, srv)
frames <- &iop.ProviderTunnelFrame{
Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_BODY, RunId: "run-chat-length-local",
Body: []byte("data: {\"id\":\"chatcmpl-provider-length\",\"created\":1777001301,\"choices\":[{\"index\":0,\"delta\":{},\"finish_reason\":\"length\"}],\"usage\":{\"prompt_tokens\":5,\"completion_tokens\":6,\"total_tokens\":11}}\n\ndata: [DONE]\n\n"),
}
frames <- &iop.ProviderTunnelFrame{Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_END, End: true, RunId: "run-chat-length-local"}
close(frames)
rest, err := io.ReadAll(reader)
if err != nil {
t.Fatalf("read provider length terminal: %v", err)
}
wire := early + string(rest)
chunks, done := decodeHotPathChatSSE(t, wire)
if done != 1 {
t.Fatalf("DONE count=%d body=%s", done, wire)
}
assertHotPathChatChunks(t, chunks, hotPathChatChunkExpectation{
ResponseID: "chatcmpl-provider-length", Model: "virtual-model", Content: "provider-limited",
Kinds: []string{"content", "terminal"}, FinishReason: "length", PromptTokens: 5, CompletionTokens: 6,
})
if got := service.requestCount(); got != 4 {
t.Fatalf("provider submissions=%d, want 4 with no review dispatch", got)
}
if srv.lightFlows.has(requestID, srv.edgeIDValue()) {
t.Fatalf("provider length retained light state for %q", requestID)
}
assertHotPathTerminal(t, srv)
for _, internalID := range []string{requestID, snapshot.ActiveStageID, "run-chat-length-local"} {
if strings.Contains(wire, internalID) {
t.Fatalf("internal identity %q leaked: %s", internalID, wire)
}
}
}
func readHotPathSSEFrame(reader *bufio.Reader) (string, error) {
var frame strings.Builder
for {
line, err := reader.ReadString('\n')
frame.WriteString(line)
if err != nil {
return frame.String(), err
}
if line == "\n" || line == "\r\n" {
return frame.String(), nil
}
}
}
type hotPathChatChunkExpectation struct {
ResponseID, Model, Content, Reasoning string
ToolID, ToolName, ToolArgs, FinishReason string
Kinds []string
PromptTokens, CompletionTokens int
}
func assertHotPathChatChunks(t *testing.T, chunks []map[string]any, want hotPathChatChunkExpectation) {
t.Helper()
var content, reasoning, toolID, toolName, toolArgs, finish string
var kinds []string
roleCount := 0
terminalCount := 0
toolIndex := -1
promptTokens := 0
completionTokens := 0
for _, chunk := range chunks {
if chunk["id"] != want.ResponseID || chunk["model"] != want.Model {
t.Fatalf("chunk identity mismatch: %+v", chunk)
}
choices, ok := chunk["choices"].([]any)
if !ok || len(choices) != 1 {
t.Fatalf("chunk choices mismatch: %+v", chunk)
}
choice := choices[0].(map[string]any)
delta := choice["delta"].(map[string]any)
if delta["role"] == "assistant" {
roleCount++
}
if text, _ := delta["content"].(string); text != "" {
content += text
kinds = append(kinds, "content")
}
if text, _ := delta["reasoning_content"].(string); text != "" {
reasoning += text
kinds = append(kinds, "reasoning")
}
if tools, ok := delta["tool_calls"].([]any); ok {
if len(tools) != 1 {
t.Fatalf("tool delta count=%d chunk=%+v", len(tools), chunk)
}
tool := tools[0].(map[string]any)
index := int(tool["index"].(float64))
if toolIndex == -1 {
toolIndex = index
} else if toolIndex != index {
t.Fatalf("tool index changed from %d to %d", toolIndex, index)
}
if id, _ := tool["id"].(string); id != "" {
toolID = id
}
function := tool["function"].(map[string]any)
if name, _ := function["name"].(string); name != "" {
toolName = name
}
if args, _ := function["arguments"].(string); args != "" {
toolArgs += args
}
kinds = append(kinds, "tool")
}
if reason, _ := choice["finish_reason"].(string); reason != "" {
finish = reason
terminalCount++
kinds = append(kinds, "terminal")
if usage, ok := chunk["usage"].(map[string]any); ok {
promptTokens = int(usage["prompt_tokens"].(float64))
completionTokens = int(usage["completion_tokens"].(float64))
}
}
}
if roleCount != 1 || terminalCount != 1 || content != want.Content || reasoning != want.Reasoning ||
finish != want.FinishReason || promptTokens != want.PromptTokens || completionTokens != want.CompletionTokens ||
strings.Join(kinds, ",") != strings.Join(want.Kinds, ",") {
t.Fatalf("chunk aggregate mismatch: role=%d terminal=%d content=%q reasoning=%q finish=%q usage=%d/%d kinds=%v chunks=%+v",
roleCount, terminalCount, content, reasoning, finish, promptTokens, completionTokens, kinds, chunks)
}
if want.ToolName != "" {
if toolIndex != 0 || toolName != want.ToolName || toolArgs != want.ToolArgs {
t.Fatalf("tool aggregate mismatch: index=%d id=%q name=%q args=%q", toolIndex, toolID, toolName, toolArgs)
}
if want.ToolID != "" && toolID != want.ToolID {
t.Fatalf("tool id=%q, want %q", toolID, want.ToolID)
}
}
}
func decodeHotPathChatSSE(t *testing.T, body string) ([]map[string]any, int) {
t.Helper()
var chunks []map[string]any
done := 0
for _, frame := range strings.Split(body, "\n\n") {
frame = strings.TrimSpace(frame)
if frame == "" {
continue
}
if !strings.HasPrefix(frame, "data: ") {
t.Fatalf("unexpected SSE frame %q", frame)
}
data := strings.TrimSpace(strings.TrimPrefix(frame, "data: "))
if data == "[DONE]" {
done++
continue
}
var chunk map[string]any
if err := json.Unmarshal([]byte(data), &chunk); err != nil {
t.Fatalf("decode SSE chunk: %v data=%s", err, data)
}
chunks = append(chunks, chunk)
}
return chunks, done
}
func serveHotPathChatBody(t *testing.T, srv *Server, body string) *httptest.ResponseRecorder {
t.Helper()
request := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(body))
response := httptest.NewRecorder()
srv.routes().ServeHTTP(response, request)
return response
}
type hotPathChatGateStep struct {
candidate edgeservice.ProviderPoolCandidate
contentType string
body func(string) string
frames chan *iop.ProviderTunnelFrame
}
type hotPathChatGateScriptedService struct {
providerFakeRunService
mu sync.Mutex
steps []hotPathChatGateStep
requests []edgeservice.ProviderPoolDispatchRequest
}
func (s *hotPathChatGateScriptedService) SubmitProviderPool(ctx context.Context, req edgeservice.ProviderPoolDispatchRequest) (*edgeservice.ProviderPoolDispatchResult, error) {
if err := ctx.Err(); err != nil {
return nil, err
}
s.mu.Lock()
index := len(s.requests)
if index >= len(s.steps) {
s.mu.Unlock()
return nil, fmt.Errorf("unexpected Chat gate dispatch %d", index+1)
}
s.requests = append(s.requests, req)
step := s.steps[index]
s.mu.Unlock()
dispatch := edgeservice.RunDispatch{
RunID: fmt.Sprintf("run-chat-gate-%d", index+1), NodeID: "node-chat-gate",
ModelGroupKey: req.Run.ModelGroupKey, ProviderID: step.candidate.ProviderID,
ExecutionPath: string(edgeservice.ProviderPoolPathTunnel), ProfileID: step.candidate.ProfileID,
ProfileDriver: step.candidate.ProfileDriver,
ProfileCapabilities: append([]string(nil), step.candidate.ProfileCapabilities...),
}
contentType := step.contentType
if contentType == "" {
contentType = "application/json"
}
frames := step.frames
if frames == nil {
body := step.body(req.Run.Metadata["iop_logical_request_id"])
frames = hotPathTunnelFrames(body, contentType, dispatch.RunID, 1_777_001_100_000_000_000+int64(index))
}
return &edgeservice.ProviderPoolDispatchResult{
Path: edgeservice.ProviderPoolPathTunnel,
Tunnel: &fakeTunnelHandle{dispatch: dispatch, frames: frames}, DispatchInfo: dispatch,
}, nil
}
func (s *hotPathChatGateScriptedService) requestCount() int {
s.mu.Lock()
defer s.mu.Unlock()
return len(s.requests)
}
func hotPathChatMixedLocalSSE() string {
return strings.Join([]string{
`data: {"id":"chatcmpl-mixed-local","created":1777001101,"choices":[{"index":0,"delta":{"content":"local-A "},"finish_reason":null}]}`,
`data: {"id":"chatcmpl-mixed-local","created":1777001101,"choices":[{"index":0,"delta":{"reasoning_content":"local-think "},"finish_reason":null}]}`,
`data: {"id":"chatcmpl-mixed-local","created":1777001101,"choices":[{"index":0,"delta":{"content":"local-B"},"finish_reason":null}]}`,
`data: {"id":"chatcmpl-mixed-local","created":1777001101,"choices":[{"index":0,"delta":{},"finish_reason":"stop"}],"usage":{"prompt_tokens":5,"completion_tokens":4,"total_tokens":9}}`,
`data: [DONE]`, "",
}, "\n\n")
}
func hotPathChatMixedReviewSSE(requestID string) string {
args := hotPathChatReviewArguments(requestID)
events := []any{
map[string]any{"type": "message_start", "message": map[string]any{
"id": "msg-mixed-review", "type": "message", "role": "assistant", "content": []any{},
"usage": map[string]any{"input_tokens": 7, "output_tokens": 0},
}},
map[string]any{"type": "content_block_start", "index": 0, "content_block": map[string]any{"type": "thinking", "thinking": "review-think ", "signature": ""}},
map[string]any{"type": "content_block_start", "index": 1, "content_block": map[string]any{"type": "text", "text": "review-visible"}},
map[string]any{"type": "content_block_start", "index": 2, "content_block": map[string]any{"type": "tool_use", "id": "provider-review-write", "name": "write_file", "input": json.RawMessage(args)}},
map[string]any{"type": "message_delta", "delta": map[string]any{"stop_reason": "tool_use", "stop_sequence": nil}, "usage": map[string]any{"output_tokens": 3}},
map[string]any{"type": "message_stop"},
}
var builder strings.Builder
for _, event := range events {
encoded, _ := json.Marshal(event)
fmt.Fprintf(&builder, "data: %s\n\n", encoded)
}
return builder.String()
}
func hotPathChatReviewArguments(requestID string) string {
encoded, _ := json.Marshal(map[string]string{
"content": "review", "path": newReservedPaths(requestID).ReviewPath,
})
return string(encoded)
}