Merge pull request #2641 from Arron196/fix/channel-monitor-responses-reasoning
fix(channel-monitor): 兼容 Responses reasoning 输出
This commit is contained in:
commit
e5d6f1727f
@ -281,9 +281,54 @@ func callProvider(ctx context.Context, provider, endpoint, apiKey, model, prompt
|
||||
if err != nil {
|
||||
return "", "", status, err
|
||||
}
|
||||
if provider == MonitorProviderOpenAI && apiMode == MonitorAPIModeResponses {
|
||||
return extractOpenAIResponsesText(respBytes), string(respBytes), status, nil
|
||||
}
|
||||
return gjson.GetBytes(respBytes, adapter.textPath).String(), string(respBytes), status, nil
|
||||
}
|
||||
|
||||
// extractOpenAIResponsesText 聚合 Responses API 的最终 assistant 文本。
|
||||
// Responses 的 output 数组顺序由模型决定:reasoning / tool-call item 可能排在 message 前面,
|
||||
// 因此不能假设文本永远在 output.0.content.0.text。
|
||||
func extractOpenAIResponsesText(respBytes []byte) string {
|
||||
if text := gjson.GetBytes(respBytes, "output_text").String(); strings.TrimSpace(text) != "" {
|
||||
return text
|
||||
}
|
||||
|
||||
var texts []string
|
||||
outputs := gjson.GetBytes(respBytes, "output")
|
||||
if outputs.IsArray() {
|
||||
outputs.ForEach(func(_, output gjson.Result) bool {
|
||||
outputType := output.Get("type").String()
|
||||
if outputType != "" && outputType != "message" {
|
||||
return true
|
||||
}
|
||||
|
||||
content := output.Get("content")
|
||||
if !content.IsArray() {
|
||||
return true
|
||||
}
|
||||
|
||||
content.ForEach(func(_, block gjson.Result) bool {
|
||||
blockType := block.Get("type").String()
|
||||
if blockType != "" && blockType != "output_text" {
|
||||
return true
|
||||
}
|
||||
if text := block.Get("text").String(); strings.TrimSpace(text) != "" {
|
||||
texts = append(texts, text)
|
||||
}
|
||||
return true
|
||||
})
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
if len(texts) > 0 {
|
||||
return strings.Join(texts, "")
|
||||
}
|
||||
return gjson.GetBytes(respBytes, providerOpenAIResponsesAdapter.textPath).String()
|
||||
}
|
||||
|
||||
// mergeHeaders 把用户自定义 headers 合并到 adapter 默认 headers 上。
|
||||
// 用户值覆盖默认;命中黑名单(hop-by-hop / 由 http.Client 自管的)的 key 静默丢弃。
|
||||
func mergeHeaders(base map[string]string, opts *CheckOptions) map[string]string {
|
||||
|
||||
@ -60,10 +60,11 @@ func setupFakeAnthropic(t *testing.T, handler *captureHandler) string {
|
||||
}
|
||||
|
||||
type openAICaptureHandler struct {
|
||||
lastBody map[string]any
|
||||
lastHeaders http.Header
|
||||
lastPath string
|
||||
status int
|
||||
lastBody map[string]any
|
||||
lastHeaders http.Header
|
||||
lastPath string
|
||||
status int
|
||||
responsesLeadingReasoning bool
|
||||
}
|
||||
|
||||
func (h *openAICaptureHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
@ -82,10 +83,23 @@ func (h *openAICaptureHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
|
||||
|
||||
answer := answerFromOpenAIRequest(parsed)
|
||||
if h.lastPath == providerOpenAIResponsesPath {
|
||||
output := []map[string]any{}
|
||||
if h.responsesLeadingReasoning {
|
||||
output = append(output, map[string]any{
|
||||
"type": "reasoning",
|
||||
"summary": []any{},
|
||||
})
|
||||
}
|
||||
output = append(output, map[string]any{
|
||||
"type": "message",
|
||||
"status": "completed",
|
||||
"role": "assistant",
|
||||
"content": []map[string]any{
|
||||
{"type": "output_text", "text": answer},
|
||||
},
|
||||
})
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||
"output": []map[string]any{{
|
||||
"content": []map[string]any{{"type": "output_text", "text": answer}},
|
||||
}},
|
||||
"output": output,
|
||||
})
|
||||
return
|
||||
}
|
||||
@ -212,6 +226,22 @@ func TestRunCheckForModel_OpenAIResponses_DefaultRequest(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunCheckForModel_OpenAIResponses_SkipsLeadingReasoningItem(t *testing.T) {
|
||||
h := &openAICaptureHandler{responsesLeadingReasoning: true}
|
||||
endpoint := setupFakeOpenAI(t, h)
|
||||
|
||||
res := runCheckForModel(context.Background(), MonitorProviderOpenAI, endpoint, "sk-openai", "gpt-5.5", &CheckOptions{
|
||||
APIMode: MonitorAPIModeResponses,
|
||||
})
|
||||
|
||||
if res.Status != MonitorStatusOperational {
|
||||
t.Fatalf("responses request should find text after leading reasoning item, got status=%s message=%q", res.Status, res.Message)
|
||||
}
|
||||
if h.lastPath != providerOpenAIResponsesPath {
|
||||
t.Fatalf("expected responses path %q, got %q", providerOpenAIResponsesPath, h.lastPath)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunCheckForModel_OpenAIResponsesReplaceMissingInstructionsFailsLocally(t *testing.T) {
|
||||
h := &openAICaptureHandler{}
|
||||
endpoint := setupFakeOpenAI(t, h)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user