fix(provider): 文本协议 - 将系统指令写入上游请求并避免重复发送
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -963,7 +963,13 @@ func manifestRequestValues(request GenerationRequest) map[string]any {
|
||||
userContent = append(userContent, map[string]any{"type": "audio_url", "audio_url": map[string]any{"url": val}})
|
||||
}
|
||||
}
|
||||
messages := make([]any, 0, len(request.Messages)+1)
|
||||
messages := make([]any, 0, len(request.Messages)+2)
|
||||
// instructions 是统一的系统指令字段,但多数 OpenAI 系协议只映射 request.messages。
|
||||
// 系统指令必须进入消息数组,否则会被静默丢弃;带独立 system 字段的协议(Claude、
|
||||
// Gemini、Responses)在各自模板里过滤 system 角色,不会重复发送。
|
||||
if instructions := strings.TrimSpace(request.Instructions); instructions != "" && !hasSystemMessage(request.Messages) {
|
||||
messages = append(messages, map[string]any{"role": "system", "content": instructions})
|
||||
}
|
||||
for _, message := range request.Messages {
|
||||
if strings.TrimSpace(message.Role) == "" || message.Content == nil {
|
||||
continue
|
||||
@@ -1028,6 +1034,15 @@ func manifestRequestValues(request GenerationRequest) map[string]any {
|
||||
}
|
||||
}
|
||||
|
||||
func hasSystemMessage(messages []Message) bool {
|
||||
for _, message := range messages {
|
||||
if strings.EqualFold(strings.TrimSpace(message.Role), "system") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func requestAsManifestValue(value any) (any, error) {
|
||||
encoded, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
|
||||
@@ -480,6 +480,55 @@ func TestOfficialOpenAIVideosDeclaresAuthenticatedResultDownload(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// 系统指令必须真正到达上游:只映射 messages 的协议要收到 system 消息,
|
||||
// 有独立 system 字段的协议要用该字段且不能在消息数组里重复发送。
|
||||
func TestOfficialTextProtocolsDeliverInstructions(t *testing.T) {
|
||||
tests := []struct {
|
||||
name, packageName, providerID, messagesField, instructionField string
|
||||
}{
|
||||
{name: "openai-chat", packageName: "openai-chat-completions.yingce-plugin", providerID: "chat-completion", messagesField: "messages"},
|
||||
{name: "deepseek", packageName: "deepseek-chat.yingce-plugin", providerID: "deepseek-chat", messagesField: "messages"},
|
||||
{name: "atlascloud", packageName: "atlascloud-chat.yingce-plugin", providerID: "atlascloud-chat", messagesField: "messages"},
|
||||
{name: "openai-responses", packageName: "openai-responses.yingce-plugin", providerID: "openai-response", messagesField: "input", instructionField: "instructions"},
|
||||
{name: "anthropic", packageName: "anthropic-messages.yingce-plugin", providerID: "claude-api", messagesField: "messages", instructionField: "system"},
|
||||
{name: "gemini", packageName: "google-gemini-generate-content.yingce-plugin", providerID: "gemini-generate-content", messagesField: "contents", instructionField: "systemInstruction"},
|
||||
}
|
||||
const instructions = "只输出一个 JSON 对象"
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
adapter := officialPackageAdapter(t, test.packageName, test.providerID)
|
||||
spec, err := adapter.BuildCreate(context.Background(), RequestContext{Request: GenerationRequest{
|
||||
Capability: CapabilityText, Model: "model-test", Prompt: "一句话故事", Instructions: instructions,
|
||||
}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
body := manifestTestBody(t, spec)
|
||||
encodedMessages, err := json.Marshal(body[test.messagesField])
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
carriesInstructions := strings.Contains(string(encodedMessages), instructions)
|
||||
if test.instructionField == "" {
|
||||
if !carriesInstructions {
|
||||
t.Fatalf("%s dropped the system instruction: %s", test.providerID, encodedMessages)
|
||||
}
|
||||
return
|
||||
}
|
||||
if carriesInstructions {
|
||||
t.Fatalf("%s duplicated the system instruction into %s: %s", test.providerID, test.messagesField, encodedMessages)
|
||||
}
|
||||
encodedInstruction, err := json.Marshal(body[test.instructionField])
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.Contains(string(encodedInstruction), instructions) {
|
||||
t.Fatalf("%s did not map the system instruction to %s: %s", test.providerID, test.instructionField, encodedInstruction)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func officialPackageAdapter(t *testing.T, packageName, providerID string) Adapter {
|
||||
t.Helper()
|
||||
data, err := os.ReadFile(filepath.Join("..", "..", "..", "plugin-packages", packageName))
|
||||
|
||||
@@ -112,7 +112,9 @@ add({
|
||||
id: "openai-responses", providerId: "openai-response", name: "OpenAI Responses", vendor: "OpenAI", capability: "text",
|
||||
baseUrl: "https://api.openai.com", auth: bearer, params: textParams,
|
||||
create: jsonCreate("/responses", {
|
||||
model: ref("request.model"), input: ref("request.messages"), instructions: omit(ref("request.instructions")),
|
||||
model: ref("request.model"),
|
||||
input: filter(ref("request.messages"), "message", ne(ref("message.role"), "system")),
|
||||
instructions: omit(ref("request.instructions")),
|
||||
temperature: omit(ref("request.providerOptions.openai-response.temperature")),
|
||||
top_p: omit(ref("request.providerOptions.openai-response.top_p")),
|
||||
max_output_tokens: omit(coalesce(ref("request.extra.max_output_tokens"), ref("request.providerOptions.openai-response.max_output_tokens"))),
|
||||
@@ -156,7 +158,7 @@ add({
|
||||
id: "google-gemini-generate-content", providerId: "gemini-generate-content", name: "Google Gemini generateContent", vendor: "Google", capability: "text",
|
||||
baseUrl: "https://generativelanguage.googleapis.com", auth: { type: "google-api-key", field: "apiKey" }, params: textParams,
|
||||
create: jsonCreate("/v1beta/models/{{model}}:generateContent", {
|
||||
contents: map(ref("request.messages"), "message", {
|
||||
contents: map(filter(ref("request.messages"), "message", ne(ref("message.role"), "system")), "message", {
|
||||
role: conditional(eq(ref("message.role"), "assistant"), "model", "user"),
|
||||
parts: [{ text: ref("message.content") }]
|
||||
}),
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user