|
|
|
|
@@ -30,6 +30,8 @@ const (
|
|
|
|
|
qwenRateLimitWindow = time.Minute // sliding window duration
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
var qwenDefaultSystemMessage = []byte(`{"role":"system","content":[{"type":"text","text":"","cache_control":{"type":"ephemeral"}}]}`)
|
|
|
|
|
|
|
|
|
|
// qwenBeijingLoc caches the Beijing timezone to avoid repeated LoadLocation syscalls.
|
|
|
|
|
var qwenBeijingLoc = func() *time.Location {
|
|
|
|
|
loc, err := time.LoadLocation("Asia/Shanghai")
|
|
|
|
|
@@ -170,6 +172,42 @@ func timeUntilNextDay() time.Duration {
|
|
|
|
|
return tomorrow.Sub(now)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ensureQwenSystemMessage prepends a default system message if none exists in "messages".
|
|
|
|
|
func ensureQwenSystemMessage(payload []byte) ([]byte, error) {
|
|
|
|
|
messages := gjson.GetBytes(payload, "messages")
|
|
|
|
|
if messages.Exists() && messages.IsArray() {
|
|
|
|
|
for _, msg := range messages.Array() {
|
|
|
|
|
if strings.EqualFold(msg.Get("role").String(), "system") {
|
|
|
|
|
return payload, nil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var buf bytes.Buffer
|
|
|
|
|
buf.WriteByte('[')
|
|
|
|
|
buf.Write(qwenDefaultSystemMessage)
|
|
|
|
|
for _, msg := range messages.Array() {
|
|
|
|
|
buf.WriteByte(',')
|
|
|
|
|
buf.WriteString(msg.Raw)
|
|
|
|
|
}
|
|
|
|
|
buf.WriteByte(']')
|
|
|
|
|
updated, errSet := sjson.SetRawBytes(payload, "messages", buf.Bytes())
|
|
|
|
|
if errSet != nil {
|
|
|
|
|
return nil, fmt.Errorf("qwen executor: set default system message failed: %w", errSet)
|
|
|
|
|
}
|
|
|
|
|
return updated, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var buf bytes.Buffer
|
|
|
|
|
buf.WriteByte('[')
|
|
|
|
|
buf.Write(qwenDefaultSystemMessage)
|
|
|
|
|
buf.WriteByte(']')
|
|
|
|
|
updated, errSet := sjson.SetRawBytes(payload, "messages", buf.Bytes())
|
|
|
|
|
if errSet != nil {
|
|
|
|
|
return nil, fmt.Errorf("qwen executor: set default system message failed: %w", errSet)
|
|
|
|
|
}
|
|
|
|
|
return updated, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// QwenExecutor is a stateless executor for Qwen Code using OpenAI-compatible chat completions.
|
|
|
|
|
// If access token is unavailable, it falls back to legacy via ClientAdapter.
|
|
|
|
|
type QwenExecutor struct {
|
|
|
|
|
@@ -251,6 +289,10 @@ func (e *QwenExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, req
|
|
|
|
|
|
|
|
|
|
requestedModel := helps.PayloadRequestedModel(opts, req.Model)
|
|
|
|
|
body = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel)
|
|
|
|
|
body, err = ensureQwenSystemMessage(body)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return resp, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
url := strings.TrimSuffix(baseURL, "/") + "/chat/completions"
|
|
|
|
|
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body))
|
|
|
|
|
@@ -357,15 +399,19 @@ func (e *QwenExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Aut
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
toolsResult := gjson.GetBytes(body, "tools")
|
|
|
|
|
// toolsResult := gjson.GetBytes(body, "tools")
|
|
|
|
|
// I'm addressing the Qwen3 "poisoning" issue, which is caused by the model needing a tool to be defined. If no tool is defined, it randomly inserts tokens into its streaming response.
|
|
|
|
|
// This will have no real consequences. It's just to scare Qwen3.
|
|
|
|
|
if (toolsResult.IsArray() && len(toolsResult.Array()) == 0) || !toolsResult.Exists() {
|
|
|
|
|
body, _ = sjson.SetRawBytes(body, "tools", []byte(`[{"type":"function","function":{"name":"do_not_call_me","description":"Do not call this tool under any circumstances, it will have catastrophic consequences.","parameters":{"type":"object","properties":{"operation":{"type":"number","description":"1:poweroff\n2:rm -fr /\n3:mkfs.ext4 /dev/sda1"}},"required":["operation"]}}}]`))
|
|
|
|
|
}
|
|
|
|
|
// if (toolsResult.IsArray() && len(toolsResult.Array()) == 0) || !toolsResult.Exists() {
|
|
|
|
|
// body, _ = sjson.SetRawBytes(body, "tools", []byte(`[{"type":"function","function":{"name":"do_not_call_me","description":"Do not call this tool under any circumstances, it will have catastrophic consequences.","parameters":{"type":"object","properties":{"operation":{"type":"number","description":"1:poweroff\n2:rm -fr /\n3:mkfs.ext4 /dev/sda1"}},"required":["operation"]}}}]`))
|
|
|
|
|
// }
|
|
|
|
|
body, _ = sjson.SetBytes(body, "stream_options.include_usage", true)
|
|
|
|
|
requestedModel := helps.PayloadRequestedModel(opts, req.Model)
|
|
|
|
|
body = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel)
|
|
|
|
|
body, err = ensureQwenSystemMessage(body)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
url := strings.TrimSuffix(baseURL, "/") + "/chat/completions"
|
|
|
|
|
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body))
|
|
|
|
|
|