793 lines
37 KiB
Go
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)
|
|
}
|