feat(infra): route Gemini WS through SOCKS5 proxy (WARP)
Some checks are pending
CI / quality (push) Waiting to run
Some checks are pending
CI / quality (push) Waiting to run
- Add socks-proxy-agent dependency - Add resolveGeminiProxyAgent() helper reading GEMINI_PROXY_URL env - Apply agent to T1 and T2 Gemini WS factory defaults - No proxy when GEMINI_PROXY_URL is unset (local dev unchanged) - Tests: 311/311 green
This commit is contained in:
parent
74770b6402
commit
5263372839
7 changed files with 139 additions and 4 deletions
|
|
@ -6,6 +6,9 @@ SUPABASE_SERVICE_ROLE_KEY=xxx
|
||||||
DEEPSEEK_API_KEY=xxx
|
DEEPSEEK_API_KEY=xxx
|
||||||
GEMINI_API_KEY=xxx
|
GEMINI_API_KEY=xxx
|
||||||
DEEPGRAM_API_KEY=xxx
|
DEEPGRAM_API_KEY=xxx
|
||||||
|
# Proxy SOCKS5 optionnel pour les WS Gemini Live (ex: Cloudflare WARP sur le VPS
|
||||||
|
# prod dont l'IP est bloquée par Google). Absent = connexion directe (dev local).
|
||||||
|
GEMINI_PROXY_URL=
|
||||||
|
|
||||||
# Stripe
|
# Stripe
|
||||||
STRIPE_SECRET_KEY=xxx
|
STRIPE_SECRET_KEY=xxx
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,26 @@ Format basé sur [Keep a Changelog](https://keepachangelog.com/fr/1.1.0/).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## [Unreleased] — 2026-06-30 — Proxy SOCKS5 (Cloudflare WARP) pour les WS Gemini Live
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- `resolveGeminiProxyAgent()` (`geminiLive.ts`) — helper qui lit la variable d'environnement **optionnelle** `GEMINI_PROXY_URL` et renvoie un `SocksProxyAgent` si elle est définie, sinon `undefined` (connexion directe). Contexte : l'IP du VPS de production (datacenter) est bloquée par Google ; Cloudflare WARP tourne en mode proxy SOCKS5 sur le VPS (`socks5://127.0.0.1:40000`). Seul le trafic WS Gemini est routé via ce proxy ; Supabase, DeepSeek et les clients restent en direct.
|
||||||
|
- Dépendance `socks-proxy-agent` (`package.json`).
|
||||||
|
- `GEMINI_PROXY_URL` ajoutée à `.env.example` (vide par défaut → dev local inchangé).
|
||||||
|
- Tests `geminiLive.test.ts` — 2 tests pour `resolveGeminiProxyAgent` (absente → `undefined` ; `socks5://…` → instance `SocksProxyAgent`).
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Factory WS par défaut de `openGeminiLiveSession` (T2, `geminiLive.ts`) et `openGeminiLiveT1Session` (T1, `geminiLiveT1.ts`) — passe désormais `{ agent }` au constructeur `new WebSocket(url, options)` quand un proxy est résolu. La factory injectée par les tests (`clientFactory`) n'est pas affectée. Prompt système, interruption, flush, `runT1/T2LiveCorrection` et close codes inchangés.
|
||||||
|
|
||||||
|
### Notes
|
||||||
|
|
||||||
|
- Tests backend : 311/311 verts. `tsc --noEmit` OK.
|
||||||
|
- Activation en prod : définir `GEMINI_PROXY_URL=socks5://127.0.0.1:40000` côté VPS/Render (config d'env, hors code).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## [Unreleased] — 2026-06-30 — Sprint 7a-patch — T1 Live : suppression de la dépendance au questionnaire
|
## [Unreleased] — 2026-06-30 — Sprint 7a-patch — T1 Live : suppression de la dépendance au questionnaire
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
|
||||||
59
package-lock.json
generated
59
package-lock.json
generated
|
|
@ -13,6 +13,7 @@
|
||||||
"@supabase/supabase-js": "^2.49.4",
|
"@supabase/supabase-js": "^2.49.4",
|
||||||
"dotenv": "^17.4.2",
|
"dotenv": "^17.4.2",
|
||||||
"hono": "^4.7.7",
|
"hono": "^4.7.7",
|
||||||
|
"socks-proxy-agent": "^10.1.0",
|
||||||
"stripe": "^17.7.0",
|
"stripe": "^17.7.0",
|
||||||
"ws": "^8.20.0"
|
"ws": "^8.20.0"
|
||||||
},
|
},
|
||||||
|
|
@ -1275,6 +1276,15 @@
|
||||||
"url": "https://opencollective.com/vitest"
|
"url": "https://opencollective.com/vitest"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/agent-base": {
|
||||||
|
"version": "9.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-9.0.0.tgz",
|
||||||
|
"integrity": "sha512-TQf59BsZnytt8GdJKLPfUZ54g/iaUL2OWDSFCCvMOhsHduDQxO8xC4PNeyIkVcA5KwL2phPSv0douC0fgWzmnA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 20"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ansi-regex": {
|
"node_modules/ansi-regex": {
|
||||||
"version": "6.2.2",
|
"version": "6.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
|
||||||
|
|
@ -1451,7 +1461,6 @@
|
||||||
"version": "4.4.3",
|
"version": "4.4.3",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ms": "^2.1.3"
|
"ms": "^2.1.3"
|
||||||
|
|
@ -1849,6 +1858,15 @@
|
||||||
"node": ">=20.0.0"
|
"node": ">=20.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ip-address": {
|
||||||
|
"version": "10.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz",
|
||||||
|
"integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/is-fullwidth-code-point": {
|
"node_modules/is-fullwidth-code-point": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||||
|
|
@ -2034,7 +2052,6 @@
|
||||||
"version": "2.1.3",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/nanoid": {
|
"node_modules/nanoid": {
|
||||||
|
|
@ -2366,6 +2383,44 @@
|
||||||
"url": "https://github.com/sponsors/isaacs"
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/smart-buffer": {
|
||||||
|
"version": "4.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
|
||||||
|
"integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6.0.0",
|
||||||
|
"npm": ">= 3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/socks": {
|
||||||
|
"version": "2.8.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/socks/-/socks-2.8.9.tgz",
|
||||||
|
"integrity": "sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ip-address": "^10.1.1",
|
||||||
|
"smart-buffer": "^4.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10.0.0",
|
||||||
|
"npm": ">= 3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/socks-proxy-agent": {
|
||||||
|
"version": "10.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-10.1.0.tgz",
|
||||||
|
"integrity": "sha512-WlMj/67cEJ6MDI1OcsnjuYKDNDoyPCCYZ249kuuXPiMDw9F8PXkVaQ7YWu3siTydfQ/4BEZcvGzu+aYvz7dDCQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"agent-base": "9.0.0",
|
||||||
|
"debug": "^4.3.4",
|
||||||
|
"socks": "^2.8.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 20"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/source-map-js": {
|
"node_modules/source-map-js": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
"@supabase/supabase-js": "^2.49.4",
|
"@supabase/supabase-js": "^2.49.4",
|
||||||
"dotenv": "^17.4.2",
|
"dotenv": "^17.4.2",
|
||||||
"hono": "^4.7.7",
|
"hono": "^4.7.7",
|
||||||
|
"socks-proxy-agent": "^10.1.0",
|
||||||
"stripe": "^17.7.0",
|
"stripe": "^17.7.0",
|
||||||
"ws": "^8.20.0"
|
"ws": "^8.20.0"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
||||||
import { EventEmitter } from "node:events";
|
import { EventEmitter } from "node:events";
|
||||||
|
import { SocksProxyAgent } from "socks-proxy-agent";
|
||||||
import {
|
import {
|
||||||
openGeminiLiveSession,
|
openGeminiLiveSession,
|
||||||
buildT2SystemPrompt,
|
buildT2SystemPrompt,
|
||||||
|
resolveGeminiProxyAgent,
|
||||||
GEMINI_LIVE_MODEL,
|
GEMINI_LIVE_MODEL,
|
||||||
type WebSocketLike,
|
type WebSocketLike,
|
||||||
} from "../geminiLive";
|
} from "../geminiLive";
|
||||||
|
|
@ -44,6 +46,32 @@ describe("buildT2SystemPrompt", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("resolveGeminiProxyAgent", () => {
|
||||||
|
let originalProxy: string | undefined;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
originalProxy = process.env.GEMINI_PROXY_URL;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
if (originalProxy === undefined) {
|
||||||
|
delete process.env.GEMINI_PROXY_URL;
|
||||||
|
} else {
|
||||||
|
process.env.GEMINI_PROXY_URL = originalProxy;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("retourne undefined quand GEMINI_PROXY_URL est absente (connexion directe)", () => {
|
||||||
|
delete process.env.GEMINI_PROXY_URL;
|
||||||
|
expect(resolveGeminiProxyAgent()).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("retourne un SocksProxyAgent quand GEMINI_PROXY_URL est définie", () => {
|
||||||
|
process.env.GEMINI_PROXY_URL = "socks5://127.0.0.1:40000";
|
||||||
|
expect(resolveGeminiProxyAgent()).toBeInstanceOf(SocksProxyAgent);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("openGeminiLiveSession (raw WS)", () => {
|
describe("openGeminiLiveSession (raw WS)", () => {
|
||||||
let originalKey: string | undefined;
|
let originalKey: string | undefined;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,23 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { WebSocket as NodeWebSocket } from "ws";
|
import { WebSocket as NodeWebSocket } from "ws";
|
||||||
|
import { SocksProxyAgent } from "socks-proxy-agent";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Résout l'agent proxy SOCKS5 pour les connexions WebSocket vers Gemini Live.
|
||||||
|
*
|
||||||
|
* Contexte : l'IP du VPS de production (datacenter) est bloquée par Google.
|
||||||
|
* Cloudflare WARP tourne en mode proxy sur le VPS (socks5://127.0.0.1:40000) ;
|
||||||
|
* router UNIQUEMENT le trafic Gemini via ce proxy le débloque, sans affecter
|
||||||
|
* le reste (Supabase, DeepSeek, clients).
|
||||||
|
*
|
||||||
|
* `GEMINI_PROXY_URL` est optionnelle : absente → connexion directe (dev local
|
||||||
|
* intact). Présente (ex: socks5://127.0.0.1:40000) → SocksProxyAgent.
|
||||||
|
*/
|
||||||
|
export function resolveGeminiProxyAgent(): SocksProxyAgent | undefined {
|
||||||
|
const url = process.env.GEMINI_PROXY_URL;
|
||||||
|
return url ? new SocksProxyAgent(url) : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
export const GEMINI_LIVE_URL =
|
export const GEMINI_LIVE_URL =
|
||||||
"wss://generativelanguage.googleapis.com/ws/google.ai.generativelanguage.v1beta.GenerativeService.BidiGenerateContent";
|
"wss://generativelanguage.googleapis.com/ws/google.ai.generativelanguage.v1beta.GenerativeService.BidiGenerateContent";
|
||||||
|
|
@ -294,9 +311,14 @@ export function openGeminiLiveSession(
|
||||||
});
|
});
|
||||||
|
|
||||||
const url = `${GEMINI_LIVE_URL}?key=${apiKey}`;
|
const url = `${GEMINI_LIVE_URL}?key=${apiKey}`;
|
||||||
|
const proxyAgent = resolveGeminiProxyAgent();
|
||||||
const factory =
|
const factory =
|
||||||
opts.clientFactory ??
|
opts.clientFactory ??
|
||||||
((u: string) => new NodeWebSocket(u) as unknown as WebSocketLike);
|
((u: string) =>
|
||||||
|
new NodeWebSocket(
|
||||||
|
u,
|
||||||
|
proxyAgent ? { agent: proxyAgent } : undefined,
|
||||||
|
) as unknown as WebSocketLike);
|
||||||
|
|
||||||
console.log("[T2] Gemini WS URL:", GEMINI_LIVE_URL + "?key=***");
|
console.log("[T2] Gemini WS URL:", GEMINI_LIVE_URL + "?key=***");
|
||||||
console.log("[T2] Gemini WS model:", GEMINI_LIVE_MODEL);
|
console.log("[T2] Gemini WS model:", GEMINI_LIVE_MODEL);
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ import {
|
||||||
isEndSignal,
|
isEndSignal,
|
||||||
parseAudioChunk,
|
parseAudioChunk,
|
||||||
reconstructTranscript,
|
reconstructTranscript,
|
||||||
|
resolveGeminiProxyAgent,
|
||||||
tryParseGeminiJson,
|
tryParseGeminiJson,
|
||||||
type TranscriptEntry,
|
type TranscriptEntry,
|
||||||
type WebSocketLike,
|
type WebSocketLike,
|
||||||
|
|
@ -203,9 +204,14 @@ export function openGeminiLiveT1Session(
|
||||||
const systemPrompt = buildT1SystemPrompt();
|
const systemPrompt = buildT1SystemPrompt();
|
||||||
|
|
||||||
const url = `${GEMINI_LIVE_URL}?key=${apiKey}`;
|
const url = `${GEMINI_LIVE_URL}?key=${apiKey}`;
|
||||||
|
const proxyAgent = resolveGeminiProxyAgent();
|
||||||
const factory =
|
const factory =
|
||||||
opts.clientFactory ??
|
opts.clientFactory ??
|
||||||
((u: string) => new NodeWebSocket(u) as unknown as WebSocketLike);
|
((u: string) =>
|
||||||
|
new NodeWebSocket(
|
||||||
|
u,
|
||||||
|
proxyAgent ? { agent: proxyAgent } : undefined,
|
||||||
|
) as unknown as WebSocketLike);
|
||||||
|
|
||||||
const geminiWs = factory(url);
|
const geminiWs = factory(url);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue