feat(openai): 完善 Chat 与 Responses 参数转发
原生 Chat/Responses 改为透明转发,保留标准工具结构并保护调用方显式参数。补齐 Responses 到 Chat 的兼容转换、协议路由边界、完整响应和流式事件,并同步更新 Swagger、回归测试与真实验收脚本。 验证: - cd apps/api && env -u AI_GATEWAY_TEST_DATABASE_URL go test ./... -count=1 - pnpm openapi - pnpm lint - pnpm test - pnpm build - gofmt -l 无输出 - git diff --check 通过 风险: - Chat 回退无法等价表达的 Responses 原生能力现在会返回 unsupported_response_parameter - 真实供应商 E2E 因本地没有已启用的平台模型候选而未完成
This commit is contained in:
@@ -1,9 +1,6 @@
|
||||
package clients
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
import "testing"
|
||||
|
||||
func TestOpenAIChatOfficialParametersSurviveBoundary(t *testing.T) {
|
||||
body := map[string]any{}
|
||||
@@ -11,7 +8,7 @@ func TestOpenAIChatOfficialParametersSurviveBoundary(t *testing.T) {
|
||||
body[key] = "sentinel-" + key
|
||||
}
|
||||
body["conversationId"] = "internal"
|
||||
body["unknown"] = "must-not-leak"
|
||||
body["future_official_field"] = map[string]any{"nested": []any{"must-survive"}}
|
||||
|
||||
filtered := FilterOpenAIChatRequestBody(body)
|
||||
for key := range openAIChatRequestParameters {
|
||||
@@ -19,7 +16,10 @@ func TestOpenAIChatOfficialParametersSurviveBoundary(t *testing.T) {
|
||||
t.Fatalf("official Chat parameter %q was removed", key)
|
||||
}
|
||||
}
|
||||
for _, key := range []string{"conversationId", "unknown"} {
|
||||
if _, ok := filtered["future_official_field"]; !ok {
|
||||
t.Fatal("future Chat field was removed at the upstream boundary")
|
||||
}
|
||||
for _, key := range []string{"conversationId"} {
|
||||
if _, ok := filtered[key]; ok {
|
||||
t.Fatalf("internal/unknown parameter %q leaked upstream", key)
|
||||
}
|
||||
@@ -32,7 +32,7 @@ func TestOpenAIResponsesOfficialParametersSurviveBoundary(t *testing.T) {
|
||||
body[key] = "sentinel-" + key
|
||||
}
|
||||
body["request_id"] = "internal"
|
||||
body["unknown"] = "must-not-leak"
|
||||
body["future_official_field"] = map[string]any{"nested": []any{"must-survive"}}
|
||||
|
||||
filtered := FilterOpenAIResponsesRequestBody(body)
|
||||
for key := range openAIResponsesRequestParameters {
|
||||
@@ -40,20 +40,19 @@ func TestOpenAIResponsesOfficialParametersSurviveBoundary(t *testing.T) {
|
||||
t.Fatalf("official Responses parameter %q was removed", key)
|
||||
}
|
||||
}
|
||||
for _, key := range []string{"request_id", "unknown"} {
|
||||
if _, ok := filtered["future_official_field"]; !ok {
|
||||
t.Fatal("future Responses field was removed at the upstream boundary")
|
||||
}
|
||||
for _, key := range []string{"request_id"} {
|
||||
if _, ok := filtered[key]; ok {
|
||||
t.Fatalf("internal/unknown parameter %q leaked upstream", key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateOpenAIRequestParametersRejectsUnknownTopLevelField(t *testing.T) {
|
||||
err := ValidateOpenAIRequestParameters("responses", map[string]any{"model": "demo", "input": "hello", "rogue": true})
|
||||
if err == nil || ErrorCode(err) != "invalid_parameter" || !strings.Contains(err.Error(), "rogue") {
|
||||
t.Fatalf("expected OpenAI-style invalid_parameter for rogue field, got %v", err)
|
||||
}
|
||||
if ErrorParam(err) != "rogue" {
|
||||
t.Fatalf("expected rogue parameter attribution, got %q", ErrorParam(err))
|
||||
func TestValidateOpenAIRequestParametersAcceptsFutureTopLevelField(t *testing.T) {
|
||||
if err := ValidateOpenAIRequestParameters("responses", map[string]any{"model": "demo", "input": "hello", "future_official_field": true}); err != nil {
|
||||
t.Fatalf("future Responses fields must remain forward compatible, got %v", err)
|
||||
}
|
||||
if err := ValidateOpenAIRequestParameters("responses", map[string]any{"model": "demo", "input": "hello", "messages": []any{}, "request_id": "internal"}); err != nil {
|
||||
t.Fatalf("expected controlled Responses extensions to remain accepted, got %v", err)
|
||||
@@ -104,3 +103,34 @@ func TestResponsesFallbackMapsEquivalentCurrentParameters(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeChatRequestPreservesCustomAndLegacyFunctionCall(t *testing.T) {
|
||||
legacy := map[string]any{"name": "legacy", "arguments": "{\"x\":1}"}
|
||||
custom := map[string]any{
|
||||
"id": "call_custom", "type": "custom",
|
||||
"custom": map[string]any{"name": "shell", "input": "pwd"},
|
||||
}
|
||||
body := NormalizeChatCompletionRequestBody(map[string]any{"messages": []any{map[string]any{
|
||||
"role": "assistant", "content": nil, "function_call": legacy, "tool_calls": []any{custom},
|
||||
}}})
|
||||
messages, _ := body["messages"].([]any)
|
||||
message, _ := messages[0].(map[string]any)
|
||||
if got, ok := message["function_call"].(map[string]any); !ok || got["name"] != "legacy" || got["arguments"] != "{\"x\":1}" {
|
||||
t.Fatalf("legacy function_call changed: %+v", message)
|
||||
}
|
||||
toolCalls, _ := message["tool_calls"].([]any)
|
||||
got, _ := toolCalls[0].(map[string]any)
|
||||
gotCustom, _ := got["custom"].(map[string]any)
|
||||
if got["type"] != "custom" || got["id"] != "call_custom" || gotCustom["name"] != "shell" || gotCustom["input"] != "pwd" || got["function"] != nil {
|
||||
t.Fatalf("standard custom tool call changed: %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureOpenAIStreamUsageDoesNotOverrideCallerChoice(t *testing.T) {
|
||||
body := map[string]any{"stream_options": map[string]any{"include_usage": false, "include_obfuscation": false}}
|
||||
ensureOpenAIStreamUsage(body, "chat.completions", true)
|
||||
options, _ := body["stream_options"].(map[string]any)
|
||||
if options["include_usage"] != false || options["include_obfuscation"] != false {
|
||||
t.Fatalf("caller stream options were changed: %+v", options)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user