Full document, spreadsheet, slideshow, and diagram tooling
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge pull request 'fix: proxy AI requests through server to avoid CORS blocking' (#170) from fix/ai-chat-cors-proxy into main

scott 93e31ae3 408c2fcc

+63 -5
+58
server/index.ts
··· 409 409 res.json({ ok: true, metadata: merged }); 410 410 }); 411 411 412 + // AI gateway proxy — avoids CORS issues by keeping requests same-origin 413 + const AI_GATEWAY_URL = process.env.AI_GATEWAY_URL || 'https://ai.lobster-hake.ts.net'; 414 + 415 + app.post('/api/ai/chat/completions', async (req: Request, res: Response) => { 416 + const gatewayUrl = `${AI_GATEWAY_URL.replace(/\/$/, '')}/chat/completions`; 417 + try { 418 + const upstream = await fetch(gatewayUrl, { 419 + method: 'POST', 420 + headers: { 'Content-Type': 'application/json' }, 421 + body: JSON.stringify(req.body), 422 + }); 423 + 424 + if (!upstream.ok) { 425 + const errText = await upstream.text(); 426 + res.status(upstream.status).send(errText); 427 + return; 428 + } 429 + 430 + // Stream the response through 431 + res.status(upstream.status); 432 + for (const [key, value] of upstream.headers.entries()) { 433 + if (['content-type', 'transfer-encoding'].includes(key.toLowerCase())) { 434 + res.set(key, value); 435 + } 436 + } 437 + 438 + if (upstream.body) { 439 + const reader = upstream.body.getReader(); 440 + const push = async (): Promise<void> => { 441 + const { done, value } = await reader.read(); 442 + if (done) { res.end(); return; } 443 + res.write(value); 444 + await push(); 445 + }; 446 + await push(); 447 + } else { 448 + res.end(); 449 + } 450 + } catch (err: unknown) { 451 + const message = err instanceof Error ? err.message : 'Unknown error'; 452 + res.status(502).json({ error: { message: `AI gateway unreachable: ${message}` } }); 453 + } 454 + }); 455 + 456 + app.get('/api/ai/models', async (_req: Request, res: Response) => { 457 + try { 458 + const upstream = await fetch(`${AI_GATEWAY_URL.replace(/\/$/, '')}/v1/models`); 459 + if (!upstream.ok) { 460 + res.status(upstream.status).send(await upstream.text()); 461 + return; 462 + } 463 + res.json(await upstream.json()); 464 + } catch (err: unknown) { 465 + const message = err instanceof Error ? err.message : 'Unknown error'; 466 + res.status(502).json({ error: { message: `AI gateway unreachable: ${message}` } }); 467 + } 468 + }); 469 + 412 470 // Health check 413 471 app.get('/health', (_req: Request, res: Response) => { 414 472 try {
+2 -2
src/lib/ai-chat.ts
··· 33 33 const LS_ENDPOINT = 'tools-ai-endpoint'; 34 34 const LS_MODEL = 'tools-ai-model'; 35 35 36 - const DEFAULT_ENDPOINT = 'https://ai.lobster-hake.ts.net'; 36 + const DEFAULT_ENDPOINT = '/api/ai'; 37 37 const DEFAULT_MODEL = 'claude-sonnet-4-6'; 38 38 const DEFAULT_MAX_TOKENS = 4096; 39 39 ··· 446 446 <summary>Advanced</summary> 447 447 <div class="ai-chat-settings-field"> 448 448 <label for="ai-endpoint">Endpoint</label> 449 - <input type="text" id="ai-endpoint" placeholder="https://ai.lobster-hake.ts.net" spellcheck="false" autocomplete="off"> 449 + <input type="text" id="ai-endpoint" placeholder="/api/ai" spellcheck="false" autocomplete="off"> 450 450 </div> 451 451 </details> 452 452 </div>
+3 -3
tests/ai-chat.test.ts
··· 44 44 45 45 it('loadConfig returns defaults when localStorage is empty', () => { 46 46 const cfg = loadConfig(); 47 - expect(cfg.endpoint).toBe('https://ai.lobster-hake.ts.net'); 47 + expect(cfg.endpoint).toBe('/api/ai'); 48 48 expect(cfg.model).toBe('claude-sonnet-4-6'); 49 49 expect(cfg.maxTokens).toBe(4096); 50 50 }); ··· 470 470 it('loadConfig returns default endpoint when stored value is empty string', () => { 471 471 localStorage.setItem('tools-ai-endpoint', ''); 472 472 const cfg = loadConfig(); 473 - expect(cfg.endpoint).toBe('https://ai.lobster-hake.ts.net'); 473 + expect(cfg.endpoint).toBe('/api/ai'); 474 474 }); 475 475 476 476 it('config persistence round-trip preserves all fields', () => { ··· 1252 1252 it('getConfig returns initial config', () => { 1253 1253 const { wiring } = setup(); 1254 1254 const cfg = wiring.getConfig(); 1255 - expect(cfg.endpoint).toBe('https://ai.lobster-hake.ts.net'); 1255 + expect(cfg.endpoint).toBe('/api/ai'); 1256 1256 expect(cfg.model).toBe('claude-sonnet-4-6'); 1257 1257 }); 1258 1258