package lua import ( "encoding/json" "strings" "testing" ) // Gemini(Cloud Code)走**私有信封协议**,不是公开的 /v1beta/models/…。 // 参考 gemini.ts 点名三件必须保留: // 1. 身份五头**逐字写死、不许随机**,且**不带** x-goog-api-key // 2. 流式刻意**不带** Accept // 3. 信封**逐层字母序**序列化(上游按字节比对) // // 此前我们的 gemini.lua 产的是公开的 contents/parts 裸体、且一个身份头都不发, // 端点还指着公开路径 —— 凭据与协议都不匹配。 func TestChannelsGeminiPrivateProtocol(t *testing.T) { vm, ps, _ := runtimePlugins(t) src, err := bundledAdapters.ReadFile("plugins/channels.lua") if err != nil { t.Fatal(err) } if err := ps.LoadSource("channels", string(src)); err != nil { t.Fatal(err) } names := map[string]bool{} for _, a := range vm.ListAdapters() { names[a.Name] = true } if !names["channels-gemini"] { t.Fatalf("插件未装 channels-gemini;已装: %v", names) } // 内置 gemini(公开 API 用)不得被顶掉 if !names["gemini"] { t.Error("内置 gemini 适配器被顶掉了") } if ep := vm.Endpoint("channels-gemini"); ep != "/v1internal:generateContent" { t.Errorf("端点应为私有动词路径,实得 %q", ep) } if se := vm.StreamEndpoint("channels-gemini"); !strings.Contains(se, "streamGenerateContent") { t.Errorf("流式端点不对: %q", se) } // 身份五头 + 不得带 google api key hdrs, err := vm.BuildHeaders("channels-gemini", map[string]interface{}{"api_key": "tok"}) if err != nil { t.Fatalf("BuildHeaders: %v", err) } for k, want := range map[string]string{ "User-Agent": "antigravity/4.3.0 (cmdc-pak)", "x-client-name": "antigravity", "x-client-version": "4.3.0", "x-machine-id": "cmdc-pak", "x-vscode-sessionid": "proxy", } { if hdrs[k] != want { t.Errorf("身份头 %s 应为 %q,实得 %q", k, want, hdrs[k]) } } for _, bad := range []string{"x-goog-api-key", "x-goog-api-client", "Accept"} { if _, ok := hdrs[bad]; ok { t.Errorf("**不得**发 %s 头(参考点名)", bad) } } // 信封:model/project/request/requestId/userAgent 五件 + request 内层 in := `{"model":"gemini-3.1-flash","messages":[` + `{"role":"system","content":"sys"},` + `{"role":"user","content":"hello"}],` + `"max_tokens":100,"stream":true}` out, err := vm.Transform("channels-gemini", "transform_request", in) if err != nil { t.Fatalf("Transform: %v", err) } t.Logf("信封: %s", out) var env map[string]interface{} if err := json.Unmarshal([]byte(out), &env); err != nil { t.Fatalf("信封不是合法 JSON: %v\n%s", err, out) } for _, k := range []string{"model", "project", "request", "requestId", "userAgent"} { if _, ok := env[k]; !ok { t.Errorf("信封缺顶层字段 %s(私有端点要信封,不是裸 contents)", k) } } if env["userAgent"] != "antigravity" { t.Errorf("userAgent 应为 antigravity,实得 %v", env["userAgent"]) } req, _ := env["request"].(map[string]interface{}) if req == nil { t.Fatal("request 不是对象") } if _, ok := req["contents"]; !ok { t.Error("request 缺 contents") } if _, ok := req["systemInstruction"]; !ok { t.Error("system 消息应进 systemInstruction") } if _, ok := req["sessionId"]; !ok { t.Error("request 缺 sessionId") } // ③ 字母序:顶层键与信封内层都必须有序(上游按字节比对) if !isSortedJSONObject(out) { t.Errorf("信封未按键字母序序列化(参考 marshalAlphabetical):\n%s", out) } // ④ 响应必须**剥一层**信封({response:{candidates}})—— 不剥则拿不到 // candidates,表现为"模型没回话" respIn := `{"response":{"candidates":[{"content":{"parts":[{"text":"hi"}]},` + `"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":3,` + `"candidatesTokenCount":2,"totalTokenCount":5}}}` rout, err := vm.Transform("channels-gemini", "transform_response", respIn) if err != nil { t.Fatalf("transform_response: %v", err) } var ru map[string]interface{} if err := json.Unmarshal([]byte(rout), &ru); err != nil { t.Fatalf("响应不是合法 JSON: %v\n%s", err, rout) } if ru["content"] != "hi" { t.Errorf("响应未剥信封(content 应来自 response.candidates[0]):%s", rout) } if ru["finish_reason"] != "stop" { t.Errorf("finishReason STOP 应映射为 stop,实得 %v", ru["finish_reason"]) } tu, _ := ru["token_usage"].(map[string]interface{}) if tu == nil || tu["prompt"] != float64(3) { t.Errorf("usageMetadata 未映射到 token_usage:%v", ru["token_usage"]) } } // isSortedJSONObject 检查 JSON 对象键是否字母序(只查顶层,够用)。 func isSortedJSONObject(s string) bool { s = strings.TrimSpace(s) if !strings.HasPrefix(s, "{") { return false } dec := json.NewDecoder(strings.NewReader(s)) tok, err := dec.Token() if err != nil { return false } if d, ok := tok.(json.Delim); !ok || d != '{' { return false } keys := []string{} for dec.More() { k, err := dec.Token() if err != nil { return false } keys = append(keys, k.(string)) var skip json.RawMessage if err := dec.Decode(&skip); err != nil { return false } } for i := 1; i < len(keys); i++ { if keys[i-1] > keys[i] { return false } } return true }