proxy-forwarder.test.ts 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310
  1. import { describe, expect, it } from "vitest";
  2. import type { Provider } from "@/types/provider";
  3. import { DEFAULT_CODEX_USER_AGENT, ProxyForwarder } from "@/app/v1/_lib/proxy/forwarder";
  4. import { ProxySession } from "@/app/v1/_lib/proxy/session";
  5. function createSession({
  6. userAgent,
  7. headers,
  8. }: {
  9. userAgent: string | null;
  10. headers: Headers;
  11. }): ProxySession {
  12. // 使用 ProxySession 的内部构造方法创建测试实例
  13. const session = Object.create(ProxySession.prototype);
  14. Object.assign(session, {
  15. startTime: Date.now(),
  16. method: "POST",
  17. requestUrl: new URL("https://example.com/v1/messages"),
  18. headers,
  19. originalHeaders: new Headers(headers), // 同步更新 originalHeaders
  20. headerLog: JSON.stringify(Object.fromEntries(headers.entries())),
  21. request: { message: {}, log: "" },
  22. userAgent,
  23. context: null,
  24. clientAbortSignal: null,
  25. userName: "test-user",
  26. authState: null,
  27. provider: null,
  28. messageContext: null,
  29. sessionId: null,
  30. requestSequence: 1,
  31. originalFormat: "claude",
  32. providerType: null,
  33. originalModelName: null,
  34. originalUrlPathname: null,
  35. providerChain: [],
  36. cacheTtlResolved: null,
  37. context1mApplied: false,
  38. cachedPriceData: undefined,
  39. cachedBillingModelSource: undefined,
  40. isHeaderModified: (key: string) => {
  41. // 简化的 isHeaderModified 实现
  42. const original = session.originalHeaders?.get(key);
  43. const current = session.headers.get(key);
  44. return original !== current;
  45. },
  46. });
  47. return session as any;
  48. }
  49. function createCodexProvider(): Provider {
  50. return {
  51. providerType: "codex",
  52. url: "https://example.com/v1/responses",
  53. key: "test-outbound-key",
  54. preserveClientIp: false,
  55. } as unknown as Provider;
  56. }
  57. function createGeminiProvider(providerType: "gemini" | "gemini-cli"): Provider {
  58. return {
  59. providerType,
  60. url: "https://generativelanguage.googleapis.com/v1beta",
  61. key: "test-outbound-key",
  62. preserveClientIp: false,
  63. } as unknown as Provider;
  64. }
  65. describe("ProxyForwarder - buildHeaders User-Agent resolution", () => {
  66. it("应该优先使用过滤器修改的 user-agent(Codex provider)", () => {
  67. const session = createSession({
  68. userAgent: "Original-UA/1.0",
  69. headers: new Headers([["user-agent", "Filtered-UA/2.0"]]),
  70. });
  71. // 设置 originalHeaders 为不同值以模拟过滤器修改
  72. (session as any).originalHeaders = new Headers([["user-agent", "Original-UA/1.0"]]);
  73. const provider = createCodexProvider();
  74. const { buildHeaders } = ProxyForwarder as unknown as {
  75. buildHeaders: (session: ProxySession, provider: Provider) => Headers;
  76. };
  77. const resultHeaders = buildHeaders(session, provider);
  78. expect(resultHeaders.get("user-agent")).toBe("Filtered-UA/2.0");
  79. });
  80. it("应该使用原始 user-agent 当未被过滤器修改时", () => {
  81. const session = createSession({
  82. userAgent: "Original-UA/1.0",
  83. headers: new Headers([["user-agent", "Original-UA/1.0"]]),
  84. });
  85. // 原始和当前相同
  86. (session as any).originalHeaders = new Headers([["user-agent", "Original-UA/1.0"]]);
  87. const provider = createCodexProvider();
  88. const { buildHeaders } = ProxyForwarder as unknown as {
  89. buildHeaders: (session: ProxySession, provider: Provider) => Headers;
  90. };
  91. const resultHeaders = buildHeaders(session, provider);
  92. expect(resultHeaders.get("user-agent")).toBe("Original-UA/1.0");
  93. });
  94. it("应该使用原始 user-agent 当过滤器删除 header 时", () => {
  95. const session = createSession({
  96. userAgent: "Original-UA/1.0",
  97. headers: new Headers(), // user-agent 被删除
  98. });
  99. // originalHeaders 包含 user-agent,但当前 headers 没有
  100. (session as any).originalHeaders = new Headers([["user-agent", "Original-UA/1.0"]]);
  101. const provider = createCodexProvider();
  102. const { buildHeaders } = ProxyForwarder as unknown as {
  103. buildHeaders: (session: ProxySession, provider: Provider) => Headers;
  104. };
  105. const resultHeaders = buildHeaders(session, provider);
  106. expect(resultHeaders.get("user-agent")).toBe("Original-UA/1.0");
  107. });
  108. it("应该使用兜底 user-agent 当原始值为空且未修改时", () => {
  109. const session = createSession({
  110. userAgent: null,
  111. headers: new Headers(),
  112. });
  113. (session as any).originalHeaders = new Headers();
  114. const provider = createCodexProvider();
  115. const { buildHeaders } = ProxyForwarder as unknown as {
  116. buildHeaders: (session: ProxySession, provider: Provider) => Headers;
  117. };
  118. const resultHeaders = buildHeaders(session, provider);
  119. expect(resultHeaders.get("user-agent")).toBe(DEFAULT_CODEX_USER_AGENT);
  120. });
  121. it("应该保留过滤器设置的空字符串 user-agent", () => {
  122. const session = createSession({
  123. userAgent: "Original-UA/1.0",
  124. headers: new Headers([["user-agent", ""]]), // 空字符串
  125. });
  126. // originalHeaders 包含原始 UA,但当前是空字符串
  127. (session as any).originalHeaders = new Headers([["user-agent", "Original-UA/1.0"]]);
  128. const provider = createCodexProvider();
  129. const { buildHeaders } = ProxyForwarder as unknown as {
  130. buildHeaders: (session: ProxySession, provider: Provider) => Headers;
  131. };
  132. const resultHeaders = buildHeaders(session, provider);
  133. // 空字符串应该被保留(使用 ?? 而非 ||)
  134. expect(resultHeaders.get("user-agent")).toBe("");
  135. });
  136. });
  137. describe("ProxyForwarder - buildGeminiHeaders headers passthrough", () => {
  138. it("应该透传 user-agent,并覆盖上游 x-goog-api-key(API key 模式)", () => {
  139. const session = createSession({
  140. userAgent: "Original-UA/1.0",
  141. headers: new Headers([
  142. ["user-agent", "Original-UA/1.0"],
  143. ["x-api-key", "proxy-user-key-should-not-leak"],
  144. ["x-goog-api-key", "proxy-user-key-google-should-not-leak"],
  145. ["authorization", "Bearer proxy-user-bearer-should-not-leak"],
  146. ]),
  147. });
  148. const provider = createGeminiProvider("gemini");
  149. const { buildGeminiHeaders } = ProxyForwarder as unknown as {
  150. buildGeminiHeaders: (
  151. session: ProxySession,
  152. provider: Provider,
  153. baseUrl: string,
  154. accessToken: string,
  155. isApiKey: boolean
  156. ) => Headers;
  157. };
  158. const resultHeaders = buildGeminiHeaders(
  159. session,
  160. provider,
  161. "https://generativelanguage.googleapis.com/v1beta",
  162. "upstream-api-key",
  163. true
  164. );
  165. expect(resultHeaders.get("user-agent")).toBe("Original-UA/1.0");
  166. expect(resultHeaders.get("x-api-key")).toBeNull();
  167. expect(resultHeaders.get("authorization")).toBeNull();
  168. expect(resultHeaders.get("x-goog-api-key")).toBe("upstream-api-key");
  169. expect(resultHeaders.get("accept-encoding")).toBe("identity");
  170. expect(resultHeaders.get("content-type")).toBe("application/json");
  171. expect(resultHeaders.get("host")).toBe("generativelanguage.googleapis.com");
  172. });
  173. it("应该允许过滤器修改 user-agent(Gemini provider)", () => {
  174. const session = createSession({
  175. userAgent: "Original-UA/1.0",
  176. headers: new Headers([["user-agent", "Filtered-UA/2.0"]]),
  177. });
  178. (session as any).originalHeaders = new Headers([["user-agent", "Original-UA/1.0"]]);
  179. const provider = createGeminiProvider("gemini");
  180. const { buildGeminiHeaders } = ProxyForwarder as unknown as {
  181. buildGeminiHeaders: (
  182. session: ProxySession,
  183. provider: Provider,
  184. baseUrl: string,
  185. accessToken: string,
  186. isApiKey: boolean
  187. ) => Headers;
  188. };
  189. const resultHeaders = buildGeminiHeaders(
  190. session,
  191. provider,
  192. "https://generativelanguage.googleapis.com/v1beta",
  193. "upstream-api-key",
  194. true
  195. );
  196. expect(resultHeaders.get("user-agent")).toBe("Filtered-UA/2.0");
  197. });
  198. it("应该在过滤器删除 user-agent 时回退到原始 userAgent(Gemini provider)", () => {
  199. const session = createSession({
  200. userAgent: "Original-UA/1.0",
  201. headers: new Headers(), // user-agent 被删除
  202. });
  203. (session as any).originalHeaders = new Headers([["user-agent", "Original-UA/1.0"]]);
  204. const provider = createGeminiProvider("gemini");
  205. const { buildGeminiHeaders } = ProxyForwarder as unknown as {
  206. buildGeminiHeaders: (
  207. session: ProxySession,
  208. provider: Provider,
  209. baseUrl: string,
  210. accessToken: string,
  211. isApiKey: boolean
  212. ) => Headers;
  213. };
  214. const resultHeaders = buildGeminiHeaders(
  215. session,
  216. provider,
  217. "https://generativelanguage.googleapis.com/v1beta",
  218. "upstream-api-key",
  219. true
  220. );
  221. expect(resultHeaders.get("user-agent")).toBe("Original-UA/1.0");
  222. });
  223. it("应该使用 Authorization Bearer(OAuth 模式)并移除 x-goog-api-key", () => {
  224. const session = createSession({
  225. userAgent: "Original-UA/1.0",
  226. headers: new Headers([
  227. ["user-agent", "Original-UA/1.0"],
  228. ["x-goog-api-key", "proxy-user-key-google-should-not-leak"],
  229. ]),
  230. });
  231. const provider = createGeminiProvider("gemini");
  232. const { buildGeminiHeaders } = ProxyForwarder as unknown as {
  233. buildGeminiHeaders: (
  234. session: ProxySession,
  235. provider: Provider,
  236. baseUrl: string,
  237. accessToken: string,
  238. isApiKey: boolean
  239. ) => Headers;
  240. };
  241. const resultHeaders = buildGeminiHeaders(
  242. session,
  243. provider,
  244. "https://generativelanguage.googleapis.com/v1beta",
  245. "upstream-oauth-token",
  246. false
  247. );
  248. expect(resultHeaders.get("authorization")).toBe("Bearer upstream-oauth-token");
  249. expect(resultHeaders.get("x-goog-api-key")).toBeNull();
  250. });
  251. it("gemini-cli 应该注入 x-goog-api-client 头", () => {
  252. const session = createSession({
  253. userAgent: "Original-UA/1.0",
  254. headers: new Headers([["user-agent", "Original-UA/1.0"]]),
  255. });
  256. const provider = createGeminiProvider("gemini-cli");
  257. const { buildGeminiHeaders } = ProxyForwarder as unknown as {
  258. buildGeminiHeaders: (
  259. session: ProxySession,
  260. provider: Provider,
  261. baseUrl: string,
  262. accessToken: string,
  263. isApiKey: boolean
  264. ) => Headers;
  265. };
  266. const resultHeaders = buildGeminiHeaders(
  267. session,
  268. provider,
  269. "https://cloudcode-pa.googleapis.com/v1internal",
  270. "upstream-oauth-token",
  271. false
  272. );
  273. expect(resultHeaders.get("x-goog-api-client")).toBe("GeminiCLI/1.0");
  274. });
  275. });