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:
2026-08-04 19:26:48 +08:00
parent b2c9b4f6d9
commit fe8dcb40ca
22 changed files with 2098 additions and 309 deletions
+17 -14
View File
@@ -1012,13 +1012,11 @@ func TestOpenAIClientChatRequestNormalizesToolContext(t *testing.T) {
}
assistant, _ := messages[0].(map[string]any)
if _, ok := assistant["functionCall"]; ok {
t.Fatalf("functionCall should be converted away: %+v", assistant)
t.Fatalf("functionCall alias should be converted away: %+v", assistant)
}
toolCalls, _ := assistant["tool_calls"].([]any)
toolCall, _ := toolCalls[0].(map[string]any)
function, _ := toolCall["function"].(map[string]any)
function, _ := assistant["function_call"].(map[string]any)
if function["name"] != "lookup" || function["arguments"] != `{"q":"weather"}` {
t.Fatalf("unexpected normalized tool call: %+v", assistant)
t.Fatalf("unexpected normalized legacy function call: %+v", assistant)
}
toolMessage, _ := messages[1].(map[string]any)
if toolMessage["tool_call_id"] != "call_0" || toolMessage["toolCallId"] != nil {
@@ -1116,14 +1114,18 @@ func TestOpenAIClientChatResponseNormalizesToolCallFormats(t *testing.T) {
if message["content"] != "calling tools" {
t.Fatalf("tool_use block should be removed from content: %+v", message)
}
for _, key := range []string{"toolCalls", "function_call"} {
for _, key := range []string{"toolCalls"} {
if _, ok := message[key]; ok {
t.Fatalf("%s should be converted away: %+v", key, message)
}
}
legacyFunction, _ := message["function_call"].(map[string]any)
if legacyFunction["name"] != "legacy_lookup" || legacyFunction["arguments"] != "{\"city\":\"NYC\"}" {
t.Fatalf("canonical legacy function_call should be preserved: %+v", message)
}
toolCalls, _ := message["tool_calls"].([]any)
if len(toolCalls) != 3 {
t.Fatalf("expected 3 normalized tool calls, got %+v", message)
if len(toolCalls) != 2 {
t.Fatalf("expected 2 normalized tool calls plus legacy function_call, got %+v", message)
}
assertToolCall := func(index int, id string, name string, arguments string) {
t.Helper()
@@ -1134,8 +1136,7 @@ func TestOpenAIClientChatResponseNormalizesToolCallFormats(t *testing.T) {
}
}
assertToolCall(0, "call_camel", "camel_lookup", "{\"city\":\"SF\"}")
assertToolCall(1, "call_1", "legacy_lookup", "{\"city\":\"NYC\"}")
assertToolCall(2, "toolu_1", "anthropic_lookup", "{\"city\":\"Boston\"}")
assertToolCall(1, "toolu_1", "anthropic_lookup", "{\"city\":\"Boston\"}")
}
func TestOpenAIClientChatStreamContract(t *testing.T) {
@@ -1393,19 +1394,21 @@ func TestOpenAIClientChatStreamNormalizesToolCallFormats(t *testing.T) {
if len(captured) != 3 {
t.Fatalf("unexpected captured events: %+v", captured)
}
for _, event := range captured {
for index, event := range captured {
choices, _ := event.Event["choices"].([]any)
choice, _ := choices[0].(map[string]any)
delta, _ := choice["delta"].(map[string]any)
if _, ok := delta["function_call"]; ok {
t.Fatalf("function_call should be converted away: %+v", event.Event)
}
if _, ok := delta["functionCall"]; ok {
t.Fatalf("functionCall should be converted away: %+v", event.Event)
}
if _, ok := delta["toolCall"]; ok {
t.Fatalf("toolCall should be converted away: %+v", event.Event)
}
if index < 2 {
if _, ok := delta["function_call"]; !ok {
t.Fatalf("canonical legacy function_call should be preserved: %+v", event.Event)
}
}
}
choices, _ := response.Result["choices"].([]any)
choice, _ := choices[0].(map[string]any)