e2e_gateway_test.go 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843
  1. //go:build e2e
  2. package integration
  3. import (
  4. "bufio"
  5. "bytes"
  6. "encoding/json"
  7. "fmt"
  8. "io"
  9. "net/http"
  10. "os"
  11. "strings"
  12. "testing"
  13. "time"
  14. )
  15. var (
  16. baseURL = getEnv("BASE_URL", "http://localhost:8080")
  17. // ENDPOINT_PREFIX: 端点前缀,支持混合模式和非混合模式测试
  18. // - "" (默认): 使用 /v1/messages, /v1beta/models(混合模式,可调度 antigravity 账户)
  19. // - "/antigravity": 使用 /antigravity/v1/messages, /antigravity/v1beta/models(非混合模式,仅 antigravity 账户)
  20. endpointPrefix = getEnv("ENDPOINT_PREFIX", "")
  21. testInterval = 1 * time.Second // 测试间隔,防止限流
  22. )
  23. const (
  24. // 注意:E2E 测试请使用环境变量注入密钥,避免任何凭证进入仓库历史。
  25. // 例如:
  26. // export CLAUDE_API_KEY="sk-..."
  27. // export GEMINI_API_KEY="sk-..."
  28. claudeAPIKeyEnv = "CLAUDE_API_KEY"
  29. geminiAPIKeyEnv = "GEMINI_API_KEY"
  30. )
  31. func getEnv(key, defaultVal string) string {
  32. if v := os.Getenv(key); v != "" {
  33. return v
  34. }
  35. return defaultVal
  36. }
  37. // Claude 模型列表
  38. var claudeModels = []string{
  39. // Opus 系列
  40. "claude-opus-4-5-thinking", // 直接支持
  41. "claude-opus-4", // 映射到 claude-opus-4-5-thinking
  42. "claude-opus-4-5-20251101", // 映射到 claude-opus-4-5-thinking
  43. // Sonnet 系列
  44. "claude-sonnet-4-5", // 直接支持
  45. "claude-sonnet-4-5-thinking", // 直接支持
  46. "claude-sonnet-4-5-20250929", // 映射到 claude-sonnet-4-5-thinking
  47. "claude-3-5-sonnet-20241022", // 映射到 claude-sonnet-4-5
  48. // Haiku 系列(映射到 gemini-3-flash)
  49. "claude-haiku-4",
  50. "claude-haiku-4-5",
  51. "claude-haiku-4-5-20251001",
  52. "claude-3-haiku-20240307",
  53. }
  54. // Gemini 模型列表
  55. var geminiModels = []string{
  56. "gemini-2.5-flash",
  57. "gemini-2.5-flash-lite",
  58. "gemini-3-flash",
  59. "gemini-3-pro-low",
  60. "gemini-3-pro-high",
  61. }
  62. func TestMain(m *testing.M) {
  63. mode := "混合模式"
  64. if endpointPrefix != "" {
  65. mode = "Antigravity 模式"
  66. }
  67. claudeKeySet := strings.TrimSpace(os.Getenv(claudeAPIKeyEnv)) != ""
  68. geminiKeySet := strings.TrimSpace(os.Getenv(geminiAPIKeyEnv)) != ""
  69. fmt.Printf("\n🚀 E2E Gateway Tests - %s (prefix=%q, %s, %s=%v, %s=%v)\n\n",
  70. baseURL,
  71. endpointPrefix,
  72. mode,
  73. claudeAPIKeyEnv,
  74. claudeKeySet,
  75. geminiAPIKeyEnv,
  76. geminiKeySet,
  77. )
  78. os.Exit(m.Run())
  79. }
  80. func requireClaudeAPIKey(t *testing.T) string {
  81. t.Helper()
  82. key := strings.TrimSpace(os.Getenv(claudeAPIKeyEnv))
  83. if key == "" {
  84. t.Skipf("未设置 %s,跳过 Claude 相关 E2E 测试", claudeAPIKeyEnv)
  85. }
  86. return key
  87. }
  88. func requireGeminiAPIKey(t *testing.T) string {
  89. t.Helper()
  90. key := strings.TrimSpace(os.Getenv(geminiAPIKeyEnv))
  91. if key == "" {
  92. t.Skipf("未设置 %s,跳过 Gemini 相关 E2E 测试", geminiAPIKeyEnv)
  93. }
  94. return key
  95. }
  96. // TestClaudeModelsList 测试 GET /v1/models
  97. func TestClaudeModelsList(t *testing.T) {
  98. claudeKey := requireClaudeAPIKey(t)
  99. url := baseURL + endpointPrefix + "/v1/models"
  100. req, _ := http.NewRequest("GET", url, nil)
  101. req.Header.Set("Authorization", "Bearer "+claudeKey)
  102. client := &http.Client{Timeout: 30 * time.Second}
  103. resp, err := client.Do(req)
  104. if err != nil {
  105. t.Fatalf("请求失败: %v", err)
  106. }
  107. defer resp.Body.Close()
  108. if resp.StatusCode != 200 {
  109. body, _ := io.ReadAll(resp.Body)
  110. t.Fatalf("HTTP %d: %s", resp.StatusCode, string(body))
  111. }
  112. var result map[string]any
  113. if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
  114. t.Fatalf("解析响应失败: %v", err)
  115. }
  116. if result["object"] != "list" {
  117. t.Errorf("期望 object=list, 得到 %v", result["object"])
  118. }
  119. data, ok := result["data"].([]any)
  120. if !ok {
  121. t.Fatal("响应缺少 data 数组")
  122. }
  123. t.Logf("✅ 返回 %d 个模型", len(data))
  124. }
  125. // TestGeminiModelsList 测试 GET /v1beta/models
  126. func TestGeminiModelsList(t *testing.T) {
  127. geminiKey := requireGeminiAPIKey(t)
  128. url := baseURL + endpointPrefix + "/v1beta/models"
  129. req, _ := http.NewRequest("GET", url, nil)
  130. req.Header.Set("Authorization", "Bearer "+geminiKey)
  131. client := &http.Client{Timeout: 30 * time.Second}
  132. resp, err := client.Do(req)
  133. if err != nil {
  134. t.Fatalf("请求失败: %v", err)
  135. }
  136. defer resp.Body.Close()
  137. if resp.StatusCode != 200 {
  138. body, _ := io.ReadAll(resp.Body)
  139. t.Fatalf("HTTP %d: %s", resp.StatusCode, string(body))
  140. }
  141. var result map[string]any
  142. if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
  143. t.Fatalf("解析响应失败: %v", err)
  144. }
  145. models, ok := result["models"].([]any)
  146. if !ok {
  147. t.Fatal("响应缺少 models 数组")
  148. }
  149. t.Logf("✅ 返回 %d 个模型", len(models))
  150. }
  151. // TestClaudeMessages 测试 Claude /v1/messages 接口
  152. func TestClaudeMessages(t *testing.T) {
  153. claudeKey := requireClaudeAPIKey(t)
  154. for i, model := range claudeModels {
  155. if i > 0 {
  156. time.Sleep(testInterval)
  157. }
  158. t.Run(model+"_非流式", func(t *testing.T) {
  159. testClaudeMessage(t, claudeKey, model, false)
  160. })
  161. time.Sleep(testInterval)
  162. t.Run(model+"_流式", func(t *testing.T) {
  163. testClaudeMessage(t, claudeKey, model, true)
  164. })
  165. }
  166. }
  167. func testClaudeMessage(t *testing.T, claudeKey string, model string, stream bool) {
  168. url := baseURL + endpointPrefix + "/v1/messages"
  169. payload := map[string]any{
  170. "model": model,
  171. "max_tokens": 50,
  172. "stream": stream,
  173. "messages": []map[string]string{
  174. {"role": "user", "content": "Say 'hello' in one word."},
  175. },
  176. }
  177. body, _ := json.Marshal(payload)
  178. req, _ := http.NewRequest("POST", url, bytes.NewReader(body))
  179. req.Header.Set("Content-Type", "application/json")
  180. req.Header.Set("Authorization", "Bearer "+claudeKey)
  181. req.Header.Set("anthropic-version", "2023-06-01")
  182. client := &http.Client{Timeout: 60 * time.Second}
  183. resp, err := client.Do(req)
  184. if err != nil {
  185. t.Fatalf("请求失败: %v", err)
  186. }
  187. defer resp.Body.Close()
  188. if resp.StatusCode != 200 {
  189. respBody, _ := io.ReadAll(resp.Body)
  190. t.Fatalf("HTTP %d: %s", resp.StatusCode, string(respBody))
  191. }
  192. if stream {
  193. // 流式:读取 SSE 事件
  194. scanner := bufio.NewScanner(resp.Body)
  195. eventCount := 0
  196. for scanner.Scan() {
  197. line := scanner.Text()
  198. if strings.HasPrefix(line, "data:") {
  199. eventCount++
  200. if eventCount >= 3 {
  201. break
  202. }
  203. }
  204. }
  205. if eventCount == 0 {
  206. t.Fatal("未收到任何 SSE 事件")
  207. }
  208. t.Logf("✅ 收到 %d+ 个 SSE 事件", eventCount)
  209. } else {
  210. // 非流式:解析 JSON 响应
  211. var result map[string]any
  212. if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
  213. t.Fatalf("解析响应失败: %v", err)
  214. }
  215. if result["type"] != "message" {
  216. t.Errorf("期望 type=message, 得到 %v", result["type"])
  217. }
  218. t.Logf("✅ 收到消息响应 id=%v", result["id"])
  219. }
  220. }
  221. // TestGeminiGenerateContent 测试 Gemini /v1beta/models/:model 接口
  222. func TestGeminiGenerateContent(t *testing.T) {
  223. geminiKey := requireGeminiAPIKey(t)
  224. for i, model := range geminiModels {
  225. if i > 0 {
  226. time.Sleep(testInterval)
  227. }
  228. t.Run(model+"_非流式", func(t *testing.T) {
  229. testGeminiGenerate(t, geminiKey, model, false)
  230. })
  231. time.Sleep(testInterval)
  232. t.Run(model+"_流式", func(t *testing.T) {
  233. testGeminiGenerate(t, geminiKey, model, true)
  234. })
  235. }
  236. }
  237. func testGeminiGenerate(t *testing.T, geminiKey string, model string, stream bool) {
  238. action := "generateContent"
  239. if stream {
  240. action = "streamGenerateContent"
  241. }
  242. url := fmt.Sprintf("%s%s/v1beta/models/%s:%s", baseURL, endpointPrefix, model, action)
  243. if stream {
  244. url += "?alt=sse"
  245. }
  246. payload := map[string]any{
  247. "contents": []map[string]any{
  248. {
  249. "role": "user",
  250. "parts": []map[string]string{
  251. {"text": "Say 'hello' in one word."},
  252. },
  253. },
  254. },
  255. "generationConfig": map[string]int{
  256. "maxOutputTokens": 50,
  257. },
  258. }
  259. body, _ := json.Marshal(payload)
  260. req, _ := http.NewRequest("POST", url, bytes.NewReader(body))
  261. req.Header.Set("Content-Type", "application/json")
  262. req.Header.Set("Authorization", "Bearer "+geminiKey)
  263. client := &http.Client{Timeout: 60 * time.Second}
  264. resp, err := client.Do(req)
  265. if err != nil {
  266. t.Fatalf("请求失败: %v", err)
  267. }
  268. defer resp.Body.Close()
  269. if resp.StatusCode != 200 {
  270. respBody, _ := io.ReadAll(resp.Body)
  271. t.Fatalf("HTTP %d: %s", resp.StatusCode, string(respBody))
  272. }
  273. if stream {
  274. // 流式:读取 SSE 事件
  275. scanner := bufio.NewScanner(resp.Body)
  276. eventCount := 0
  277. for scanner.Scan() {
  278. line := scanner.Text()
  279. if strings.HasPrefix(line, "data:") {
  280. eventCount++
  281. if eventCount >= 3 {
  282. break
  283. }
  284. }
  285. }
  286. if eventCount == 0 {
  287. t.Fatal("未收到任何 SSE 事件")
  288. }
  289. t.Logf("✅ 收到 %d+ 个 SSE 事件", eventCount)
  290. } else {
  291. // 非流式:解析 JSON 响应
  292. var result map[string]any
  293. if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
  294. t.Fatalf("解析响应失败: %v", err)
  295. }
  296. if _, ok := result["candidates"]; !ok {
  297. t.Error("响应缺少 candidates 字段")
  298. }
  299. t.Log("✅ 收到 candidates 响应")
  300. }
  301. }
  302. // TestClaudeMessagesWithComplexTools 测试带复杂工具 schema 的请求
  303. // 模拟 Claude Code 发送的请求,包含需要清理的 JSON Schema 字段
  304. func TestClaudeMessagesWithComplexTools(t *testing.T) {
  305. claudeKey := requireClaudeAPIKey(t)
  306. // 测试模型列表(只测试几个代表性模型)
  307. models := []string{
  308. "claude-opus-4-5-20251101", // Claude 模型
  309. "claude-haiku-4-5-20251001", // 映射到 Gemini
  310. }
  311. for i, model := range models {
  312. if i > 0 {
  313. time.Sleep(testInterval)
  314. }
  315. t.Run(model+"_复杂工具", func(t *testing.T) {
  316. testClaudeMessageWithTools(t, claudeKey, model)
  317. })
  318. }
  319. }
  320. func testClaudeMessageWithTools(t *testing.T, claudeKey string, model string) {
  321. url := baseURL + endpointPrefix + "/v1/messages"
  322. // 构造包含复杂 schema 的工具定义(模拟 Claude Code 的工具)
  323. // 这些字段需要被 cleanJSONSchema 清理
  324. tools := []map[string]any{
  325. {
  326. "name": "read_file",
  327. "description": "Read file contents",
  328. "input_schema": map[string]any{
  329. "$schema": "http://json-schema.org/draft-07/schema#",
  330. "type": "object",
  331. "properties": map[string]any{
  332. "path": map[string]any{
  333. "type": "string",
  334. "description": "File path",
  335. "minLength": 1,
  336. "maxLength": 4096,
  337. "pattern": "^[^\\x00]+$",
  338. },
  339. "encoding": map[string]any{
  340. "type": []string{"string", "null"},
  341. "default": "utf-8",
  342. "enum": []string{"utf-8", "ascii", "latin-1"},
  343. },
  344. },
  345. "required": []string{"path"},
  346. "additionalProperties": false,
  347. },
  348. },
  349. {
  350. "name": "write_file",
  351. "description": "Write content to file",
  352. "input_schema": map[string]any{
  353. "type": "object",
  354. "properties": map[string]any{
  355. "path": map[string]any{
  356. "type": "string",
  357. "minLength": 1,
  358. },
  359. "content": map[string]any{
  360. "type": "string",
  361. "maxLength": 1048576,
  362. },
  363. },
  364. "required": []string{"path", "content"},
  365. "additionalProperties": false,
  366. "strict": true,
  367. },
  368. },
  369. {
  370. "name": "list_files",
  371. "description": "List files in directory",
  372. "input_schema": map[string]any{
  373. "$id": "https://example.com/list-files.schema.json",
  374. "type": "object",
  375. "properties": map[string]any{
  376. "directory": map[string]any{
  377. "type": "string",
  378. },
  379. "patterns": map[string]any{
  380. "type": "array",
  381. "items": map[string]any{
  382. "type": "string",
  383. "minLength": 1,
  384. },
  385. "minItems": 1,
  386. "maxItems": 100,
  387. "uniqueItems": true,
  388. },
  389. "recursive": map[string]any{
  390. "type": "boolean",
  391. "default": false,
  392. },
  393. },
  394. "required": []string{"directory"},
  395. "additionalProperties": false,
  396. },
  397. },
  398. {
  399. "name": "search_code",
  400. "description": "Search code in files",
  401. "input_schema": map[string]any{
  402. "type": "object",
  403. "properties": map[string]any{
  404. "query": map[string]any{
  405. "type": "string",
  406. "minLength": 1,
  407. "format": "regex",
  408. },
  409. "max_results": map[string]any{
  410. "type": "integer",
  411. "minimum": 1,
  412. "maximum": 1000,
  413. "exclusiveMinimum": 0,
  414. "default": 100,
  415. },
  416. },
  417. "required": []string{"query"},
  418. "additionalProperties": false,
  419. "examples": []map[string]any{
  420. {"query": "function.*test", "max_results": 50},
  421. },
  422. },
  423. },
  424. // 测试 required 引用不存在的属性(应被自动过滤)
  425. {
  426. "name": "invalid_required_tool",
  427. "description": "Tool with invalid required field",
  428. "input_schema": map[string]any{
  429. "type": "object",
  430. "properties": map[string]any{
  431. "name": map[string]any{
  432. "type": "string",
  433. },
  434. },
  435. // "nonexistent_field" 不存在于 properties 中,应被过滤掉
  436. "required": []string{"name", "nonexistent_field"},
  437. },
  438. },
  439. // 测试没有 properties 的 schema(应自动添加空 properties)
  440. {
  441. "name": "no_properties_tool",
  442. "description": "Tool without properties",
  443. "input_schema": map[string]any{
  444. "type": "object",
  445. "required": []string{"should_be_removed"},
  446. },
  447. },
  448. // 测试没有 type 的 schema(应自动添加 type: OBJECT)
  449. {
  450. "name": "no_type_tool",
  451. "description": "Tool without type",
  452. "input_schema": map[string]any{
  453. "properties": map[string]any{
  454. "value": map[string]any{
  455. "type": "string",
  456. },
  457. },
  458. },
  459. },
  460. }
  461. payload := map[string]any{
  462. "model": model,
  463. "max_tokens": 100,
  464. "stream": false,
  465. "messages": []map[string]string{
  466. {"role": "user", "content": "List files in the current directory"},
  467. },
  468. "tools": tools,
  469. }
  470. body, _ := json.Marshal(payload)
  471. req, _ := http.NewRequest("POST", url, bytes.NewReader(body))
  472. req.Header.Set("Content-Type", "application/json")
  473. req.Header.Set("Authorization", "Bearer "+claudeKey)
  474. req.Header.Set("anthropic-version", "2023-06-01")
  475. client := &http.Client{Timeout: 60 * time.Second}
  476. resp, err := client.Do(req)
  477. if err != nil {
  478. t.Fatalf("请求失败: %v", err)
  479. }
  480. defer resp.Body.Close()
  481. respBody, _ := io.ReadAll(resp.Body)
  482. // 400 错误说明 schema 清理不完整
  483. if resp.StatusCode == 400 {
  484. t.Fatalf("Schema 清理失败,收到 400 错误: %s", string(respBody))
  485. }
  486. // 503 可能是账号限流,不算测试失败
  487. if resp.StatusCode == 503 {
  488. t.Skipf("账号暂时不可用 (503): %s", string(respBody))
  489. }
  490. // 429 是限流
  491. if resp.StatusCode == 429 {
  492. t.Skipf("请求被限流 (429): %s", string(respBody))
  493. }
  494. if resp.StatusCode != 200 {
  495. t.Fatalf("HTTP %d: %s", resp.StatusCode, string(respBody))
  496. }
  497. var result map[string]any
  498. if err := json.Unmarshal(respBody, &result); err != nil {
  499. t.Fatalf("解析响应失败: %v", err)
  500. }
  501. if result["type"] != "message" {
  502. t.Errorf("期望 type=message, 得到 %v", result["type"])
  503. }
  504. t.Logf("✅ 复杂工具 schema 测试通过, id=%v", result["id"])
  505. }
  506. // TestClaudeMessagesWithThinkingAndTools 测试 thinking 模式下带工具调用的场景
  507. // 验证:当历史 assistant 消息包含 tool_use 但没有 signature 时,
  508. // 系统应自动添加 dummy thought_signature 避免 Gemini 400 错误
  509. func TestClaudeMessagesWithThinkingAndTools(t *testing.T) {
  510. claudeKey := requireClaudeAPIKey(t)
  511. models := []string{
  512. "claude-haiku-4-5-20251001", // gemini-3-flash
  513. }
  514. for i, model := range models {
  515. if i > 0 {
  516. time.Sleep(testInterval)
  517. }
  518. t.Run(model+"_thinking模式工具调用", func(t *testing.T) {
  519. testClaudeThinkingWithToolHistory(t, claudeKey, model)
  520. })
  521. }
  522. }
  523. func testClaudeThinkingWithToolHistory(t *testing.T, claudeKey string, model string) {
  524. url := baseURL + endpointPrefix + "/v1/messages"
  525. // 模拟历史对话:用户请求 → assistant 调用工具 → 工具返回 → 继续对话
  526. // 注意:tool_use 块故意不包含 signature,测试系统是否能正确添加 dummy signature
  527. payload := map[string]any{
  528. "model": model,
  529. "max_tokens": 200,
  530. "stream": false,
  531. // 开启 thinking 模式
  532. "thinking": map[string]any{
  533. "type": "enabled",
  534. "budget_tokens": 1024,
  535. },
  536. "messages": []any{
  537. map[string]any{
  538. "role": "user",
  539. "content": "List files in the current directory",
  540. },
  541. // assistant 消息包含 tool_use 但没有 signature
  542. map[string]any{
  543. "role": "assistant",
  544. "content": []map[string]any{
  545. {
  546. "type": "text",
  547. "text": "I'll list the files for you.",
  548. },
  549. {
  550. "type": "tool_use",
  551. "id": "toolu_01XGmNv",
  552. "name": "Bash",
  553. "input": map[string]any{"command": "ls -la"},
  554. // 故意不包含 signature
  555. },
  556. },
  557. },
  558. // 工具结果
  559. map[string]any{
  560. "role": "user",
  561. "content": []map[string]any{
  562. {
  563. "type": "tool_result",
  564. "tool_use_id": "toolu_01XGmNv",
  565. "content": "file1.txt\nfile2.txt\ndir1/",
  566. },
  567. },
  568. },
  569. },
  570. "tools": []map[string]any{
  571. {
  572. "name": "Bash",
  573. "description": "Execute bash commands",
  574. "input_schema": map[string]any{
  575. "type": "object",
  576. "properties": map[string]any{
  577. "command": map[string]any{
  578. "type": "string",
  579. },
  580. },
  581. "required": []string{"command"},
  582. },
  583. },
  584. },
  585. }
  586. body, _ := json.Marshal(payload)
  587. req, _ := http.NewRequest("POST", url, bytes.NewReader(body))
  588. req.Header.Set("Content-Type", "application/json")
  589. req.Header.Set("Authorization", "Bearer "+claudeKey)
  590. req.Header.Set("anthropic-version", "2023-06-01")
  591. client := &http.Client{Timeout: 60 * time.Second}
  592. resp, err := client.Do(req)
  593. if err != nil {
  594. t.Fatalf("请求失败: %v", err)
  595. }
  596. defer resp.Body.Close()
  597. respBody, _ := io.ReadAll(resp.Body)
  598. // 400 错误说明 thought_signature 处理失败
  599. if resp.StatusCode == 400 {
  600. t.Fatalf("thought_signature 处理失败,收到 400 错误: %s", string(respBody))
  601. }
  602. // 503 可能是账号限流,不算测试失败
  603. if resp.StatusCode == 503 {
  604. t.Skipf("账号暂时不可用 (503): %s", string(respBody))
  605. }
  606. // 429 是限流
  607. if resp.StatusCode == 429 {
  608. t.Skipf("请求被限流 (429): %s", string(respBody))
  609. }
  610. if resp.StatusCode != 200 {
  611. t.Fatalf("HTTP %d: %s", resp.StatusCode, string(respBody))
  612. }
  613. var result map[string]any
  614. if err := json.Unmarshal(respBody, &result); err != nil {
  615. t.Fatalf("解析响应失败: %v", err)
  616. }
  617. if result["type"] != "message" {
  618. t.Errorf("期望 type=message, 得到 %v", result["type"])
  619. }
  620. t.Logf("✅ thinking 模式工具调用测试通过, id=%v", result["id"])
  621. }
  622. // TestClaudeMessagesWithGeminiModel 测试在 Claude 端点使用 Gemini 模型
  623. // 验证:通过 /v1/messages 端点传入 gemini 模型名的场景(含前缀映射)
  624. // 仅在 Antigravity 模式下运行(ENDPOINT_PREFIX="/antigravity")
  625. func TestClaudeMessagesWithGeminiModel(t *testing.T) {
  626. if endpointPrefix != "/antigravity" {
  627. t.Skip("仅在 Antigravity 模式下运行")
  628. }
  629. claudeKey := requireClaudeAPIKey(t)
  630. // 测试通过 Claude 端点调用 Gemini 模型
  631. geminiViaClaude := []string{
  632. "gemini-3-flash", // 直接支持
  633. "gemini-3-pro-low", // 直接支持
  634. "gemini-3-pro-high", // 直接支持
  635. "gemini-3-pro", // 前缀映射 -> gemini-3-pro-high
  636. "gemini-3-pro-preview", // 前缀映射 -> gemini-3-pro-high
  637. }
  638. for i, model := range geminiViaClaude {
  639. if i > 0 {
  640. time.Sleep(testInterval)
  641. }
  642. t.Run(model+"_通过Claude端点", func(t *testing.T) {
  643. testClaudeMessage(t, claudeKey, model, false)
  644. })
  645. time.Sleep(testInterval)
  646. t.Run(model+"_通过Claude端点_流式", func(t *testing.T) {
  647. testClaudeMessage(t, claudeKey, model, true)
  648. })
  649. }
  650. }
  651. // TestClaudeMessagesWithNoSignature 测试历史 thinking block 不带 signature 的场景
  652. // 验证:Gemini 模型接受没有 signature 的 thinking block
  653. func TestClaudeMessagesWithNoSignature(t *testing.T) {
  654. claudeKey := requireClaudeAPIKey(t)
  655. models := []string{
  656. "claude-haiku-4-5-20251001", // gemini-3-flash - 支持无 signature
  657. }
  658. for i, model := range models {
  659. if i > 0 {
  660. time.Sleep(testInterval)
  661. }
  662. t.Run(model+"_无signature", func(t *testing.T) {
  663. testClaudeWithNoSignature(t, claudeKey, model)
  664. })
  665. }
  666. }
  667. func testClaudeWithNoSignature(t *testing.T, claudeKey string, model string) {
  668. url := baseURL + endpointPrefix + "/v1/messages"
  669. // 模拟历史对话包含 thinking block 但没有 signature
  670. payload := map[string]any{
  671. "model": model,
  672. "max_tokens": 200,
  673. "stream": false,
  674. // 开启 thinking 模式
  675. "thinking": map[string]any{
  676. "type": "enabled",
  677. "budget_tokens": 1024,
  678. },
  679. "messages": []any{
  680. map[string]any{
  681. "role": "user",
  682. "content": "What is 2+2?",
  683. },
  684. // assistant 消息包含 thinking block 但没有 signature
  685. map[string]any{
  686. "role": "assistant",
  687. "content": []map[string]any{
  688. {
  689. "type": "thinking",
  690. "thinking": "Let me calculate 2+2...",
  691. // 故意不包含 signature
  692. },
  693. {
  694. "type": "text",
  695. "text": "2+2 equals 4.",
  696. },
  697. },
  698. },
  699. map[string]any{
  700. "role": "user",
  701. "content": "What is 3+3?",
  702. },
  703. },
  704. }
  705. body, _ := json.Marshal(payload)
  706. req, _ := http.NewRequest("POST", url, bytes.NewReader(body))
  707. req.Header.Set("Content-Type", "application/json")
  708. req.Header.Set("Authorization", "Bearer "+claudeKey)
  709. req.Header.Set("anthropic-version", "2023-06-01")
  710. client := &http.Client{Timeout: 60 * time.Second}
  711. resp, err := client.Do(req)
  712. if err != nil {
  713. t.Fatalf("请求失败: %v", err)
  714. }
  715. defer resp.Body.Close()
  716. respBody, _ := io.ReadAll(resp.Body)
  717. if resp.StatusCode == 400 {
  718. t.Fatalf("无 signature thinking 处理失败,收到 400 错误: %s", string(respBody))
  719. }
  720. if resp.StatusCode == 503 {
  721. t.Skipf("账号暂时不可用 (503): %s", string(respBody))
  722. }
  723. if resp.StatusCode == 429 {
  724. t.Skipf("请求被限流 (429): %s", string(respBody))
  725. }
  726. if resp.StatusCode != 200 {
  727. t.Fatalf("HTTP %d: %s", resp.StatusCode, string(respBody))
  728. }
  729. var result map[string]any
  730. if err := json.Unmarshal(respBody, &result); err != nil {
  731. t.Fatalf("解析响应失败: %v", err)
  732. }
  733. if result["type"] != "message" {
  734. t.Errorf("期望 type=message, 得到 %v", result["type"])
  735. }
  736. t.Logf("✅ 无 signature thinking 处理测试通过, id=%v", result["id"])
  737. }
  738. // TestGeminiEndpointWithClaudeModel 测试通过 Gemini 端点调用 Claude 模型
  739. // 仅在 Antigravity 模式下运行(ENDPOINT_PREFIX="/antigravity")
  740. func TestGeminiEndpointWithClaudeModel(t *testing.T) {
  741. if endpointPrefix != "/antigravity" {
  742. t.Skip("仅在 Antigravity 模式下运行")
  743. }
  744. geminiKey := requireGeminiAPIKey(t)
  745. // 测试通过 Gemini 端点调用 Claude 模型
  746. claudeViaGemini := []string{
  747. "claude-sonnet-4-5",
  748. "claude-opus-4-5-thinking",
  749. }
  750. for i, model := range claudeViaGemini {
  751. if i > 0 {
  752. time.Sleep(testInterval)
  753. }
  754. t.Run(model+"_通过Gemini端点", func(t *testing.T) {
  755. testGeminiGenerate(t, geminiKey, model, false)
  756. })
  757. time.Sleep(testInterval)
  758. t.Run(model+"_通过Gemini端点_流式", func(t *testing.T) {
  759. testGeminiGenerate(t, geminiKey, model, true)
  760. })
  761. }
  762. }