From b997e5471d2f687769046b08818452b3a6116f13 Mon Sep 17 00:00:00 2001 From: guenhter Date: Tue, 21 Oct 2025 07:38:03 +0200 Subject: [PATCH 01/25] test: add frontend openai mock server --- frontend_omni/mock-openai-server.js | 210 +++++++ frontend_omni/package.json | 2 + frontend_omni/playwright.config.ts | 25 +- frontend_omni/pnpm-lock.yaml | 556 ++++++++++++++++++ frontend_omni/tests/mock-openai-helper.ts | 121 ++++ .../tests/mock-openai-server.spec.ts | 103 ++++ 6 files changed, 1012 insertions(+), 5 deletions(-) create mode 100644 frontend_omni/mock-openai-server.js create mode 100644 frontend_omni/tests/mock-openai-helper.ts create mode 100644 frontend_omni/tests/mock-openai-server.spec.ts diff --git a/frontend_omni/mock-openai-server.js b/frontend_omni/mock-openai-server.js new file mode 100644 index 0000000..88a0c36 --- /dev/null +++ b/frontend_omni/mock-openai-server.js @@ -0,0 +1,210 @@ +import express from 'express'; +import cors from 'cors'; + +const app = express(); +const PORT = process.env.MOCK_OPENAI_PORT || 3001; + +// Middleware +app.use(cors()); +app.use(express.json()); + +// Mock data storage +let mockModels = [ + { + id: 'gpt-3.5-turbo', + object: 'model', + created: 1677610602, + owned_by: 'openai' + }, + { + id: 'gpt-4', + object: 'model', + created: 1687882411, + owned_by: 'openai' + } +]; + +let mockCompletionResponse = { + id: 'chatcmpl-mock', + object: 'chat.completion', + created: Date.now(), + model: 'gpt-3.5-turbo', + choices: [{ + index: 0, + message: { + role: 'assistant', + content: 'This is a mock response from the test server.' + }, + finish_reason: 'stop' + }], + usage: { + prompt_tokens: 10, + completion_tokens: 20, + total_tokens: 30 + } +}; + +let mockStreamingResponse = [ + { + id: 'chatcmpl-mock-stream', + object: 'chat.completion.chunk', + created: Date.now(), + model: 'gpt-3.5-turbo', + choices: [{ + index: 0, + delta: { content: 'This ' }, + finish_reason: null + }] + }, + { + id: 'chatcmpl-mock-stream', + object: 'chat.completion.chunk', + created: Date.now(), + model: 'gpt-3.5-turbo', + choices: [{ + index: 0, + delta: { content: 'is ' }, + finish_reason: null + }] + }, + { + id: 'chatcmpl-mock-stream', + object: 'chat.completion.chunk', + created: Date.now(), + model: 'gpt-3.5-turbo', + choices: [{ + index: 0, + delta: { content: 'a mock streaming response.' }, + finish_reason: 'stop' + }] + } +]; + +// Routes +app.get('/', (req, res) => { + res.json({ status: 'ok', message: 'Mock OpenAI server is running' }); +}); + +app.get('/models', (req, res) => { + res.json({ + object: 'list', + data: mockModels + }); +}); + +app.post('/chat/completions', async (req, res) => { + const { stream = false } = req.body; + + if (stream) { + // Handle streaming response + res.setHeader('Content-Type', 'text/plain'); + res.setHeader('Cache-Control', 'no-cache'); + res.setHeader('Connection', 'keep-alive'); + + for (const chunk of mockStreamingResponse) { + res.write(`data: ${JSON.stringify(chunk)}\n\n`); + await new Promise(resolve => setTimeout(resolve, 100)); // Simulate delay + } + res.write('data: [DONE]\n\n'); + res.end(); + } else { + // Handle regular response + res.json(mockCompletionResponse); + } +}); + +// Admin endpoints for controlling mock responses +app.post('/admin/set-models', (req, res) => { + mockModels = req.body.models || []; + res.json({ success: true, models: mockModels }); +}); + +app.post('/admin/set-completion-response', (req, res) => { + mockCompletionResponse = req.body.response || mockCompletionResponse; + res.json({ success: true, response: mockCompletionResponse }); +}); + +app.post('/admin/set-streaming-response', (req, res) => { + mockStreamingResponse = req.body.chunks || []; + res.json({ success: true, chunks: mockStreamingResponse }); +}); + +app.post('/admin/reset', (req, res) => { + // Reset to defaults + mockModels = [ + { + id: 'gpt-3.5-turbo', + object: 'model', + created: 1677610602, + owned_by: 'openai' + }, + { + id: 'gpt-4', + object: 'model', + created: 1687882411, + owned_by: 'openai' + } + ]; + + mockCompletionResponse = { + id: 'chatcmpl-mock', + object: 'chat.completion', + created: Date.now(), + model: 'gpt-3.5-turbo', + choices: [{ + index: 0, + message: { + role: 'assistant', + content: 'This is a mock response from the test server.' + }, + finish_reason: 'stop' + }], + usage: { + prompt_tokens: 10, + completion_tokens: 20, + total_tokens: 30 + } + }; + + mockStreamingResponse = [ + { + id: 'chatcmpl-mock-stream', + object: 'chat.completion.chunk', + created: Date.now(), + model: 'gpt-3.5-turbo', + choices: [{ + index: 0, + delta: { content: 'This ' }, + finish_reason: null + }] + }, + { + id: 'chatcmpl-mock-stream', + object: 'chat.completion.chunk', + created: Date.now(), + model: 'gpt-3.5-turbo', + choices: [{ + index: 0, + delta: { content: 'is ' }, + finish_reason: null + }] + }, + { + id: 'chatcmpl-mock-stream', + object: 'chat.completion.chunk', + created: Date.now(), + model: 'gpt-3.5-turbo', + choices: [{ + index: 0, + delta: { content: 'a mock streaming response.' }, + finish_reason: 'stop' + }] + } + ]; + + res.json({ success: true }); +}); + +app.listen(PORT, () => { + console.log(`Mock OpenAI server running on port ${PORT}`); +}); diff --git a/frontend_omni/package.json b/frontend_omni/package.json index 4b6dd69..02ba111 100644 --- a/frontend_omni/package.json +++ b/frontend_omni/package.json @@ -73,6 +73,8 @@ "@types/react-syntax-highlighter": "^15.5.13", "@vitejs/plugin-react": "^5.0.0", "babel-plugin-react-compiler": "19.1.0-rc.3", + "cors": "^2.8.5", + "express": "^5.1.0", "globals": "^16.3.0", "jsdom": "^27.0.0", "playwright": "^1.56.1", diff --git a/frontend_omni/playwright.config.ts b/frontend_omni/playwright.config.ts index 61888a5..099b5c0 100644 --- a/frontend_omni/playwright.config.ts +++ b/frontend_omni/playwright.config.ts @@ -19,10 +19,18 @@ export default defineConfig({ { name: "chromium", use: { ...devices["Desktop Chrome"] }, + testIgnore: "**/mock-openai-server.spec.ts", }, { name: "firefox", use: { ...devices["Desktop Firefox"] }, + testIgnore: "**/mock-openai-server.spec.ts", + }, + { + name: "mock-tests", + testMatch: "**/mock-openai-server.spec.ts", + use: { ...devices["Desktop Chrome"] }, + workers: 1, // Run serially }, // Support this later at some point // { @@ -30,9 +38,16 @@ export default defineConfig({ // use: { ...devices["Desktop Safari"] }, // }, ], - webServer: { - command: "pnpm build && pnpm preview", - url: "http://localhost:4173", - reuseExistingServer: !process.env.CI, - }, + webServer: [ + { + command: "pnpm build && pnpm preview", + url: "http://localhost:4173", + reuseExistingServer: !process.env.CI, + }, + { + command: "node mock-openai-server.js", + url: "http://localhost:3001", + reuseExistingServer: !process.env.CI, + }, + ], }); diff --git a/frontend_omni/pnpm-lock.yaml b/frontend_omni/pnpm-lock.yaml index 11910fc..a6eeb06 100644 --- a/frontend_omni/pnpm-lock.yaml +++ b/frontend_omni/pnpm-lock.yaml @@ -177,6 +177,12 @@ importers: babel-plugin-react-compiler: specifier: 19.1.0-rc.3 version: 19.1.0-rc.3 + cors: + specifier: ^2.8.5 + version: 2.8.5 + express: + specifier: ^5.1.0 + version: 5.1.0 globals: specifier: ^16.3.0 version: 16.4.0 @@ -1628,6 +1634,10 @@ packages: '@xyflow/system@0.0.70': resolution: {integrity: sha512-PpC//u9zxdjj0tfTSmZrg3+sRbTz6kop/Amky44U2Dl51sxzDTIUfXMwETOYpmr2dqICWXBIJwXL2a9QWtX2XA==} + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + acorn-walk@8.3.4: resolution: {integrity: sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==} engines: {node: '>=0.4.0'} @@ -1690,15 +1700,31 @@ packages: bidi-js@1.0.3: resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} + body-parser@2.2.0: + resolution: {integrity: sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==} + engines: {node: '>=18'} + browserslist@4.26.2: resolution: {integrity: sha512-ECFzp6uFOSB+dcZ5BK/IBaGWssbSYBHvuMeMt3MMFyhI0Z8SqGgEkBLARgpRH3hutIgPVsALcMwbDrJqPxQ65A==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + cac@6.7.14: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + caniuse-lite@1.0.30001743: resolution: {integrity: sha512-e6Ojr7RV14Un7dz6ASD0aZDmQPT/A+eZU+nuTNfjqmRrmkmQlnTNWH0SKmqagx9PeW87UVqapSurtAXifmtdmw==} @@ -1782,13 +1808,33 @@ packages: confbox@0.2.2: resolution: {integrity: sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==} + content-disposition@1.0.0: + resolution: {integrity: sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==} + engines: {node: '>= 0.6'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + cookie@1.0.2: resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==} engines: {node: '>=18'} + cors@2.8.5: + resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} + engines: {node: '>= 0.10'} + cose-base@1.0.3: resolution: {integrity: sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==} @@ -1997,6 +2043,10 @@ packages: delaunator@5.0.1: resolution: {integrity: sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==} + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + dequal@2.0.3: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} @@ -2024,6 +2074,13 @@ packages: dompurify@3.3.0: resolution: {integrity: sha512-r+f6MYR1gGN1eJv0TVQbhA7if/U7P87cdPl3HN5rikqaBSBxLiCb/b9O+2eG0cxz0ghyU+mU1QkbsOwERMYlWQ==} + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + electron-to-chromium@1.5.222: resolution: {integrity: sha512-gA7psSwSwQRE60CEoLz6JBCQPIxNeuzB2nL8vE03GK/OHxlvykbLyeiumQy1iH5C2f3YbRAZpGCMT12a/9ih9w==} @@ -2040,6 +2097,10 @@ packages: embla-carousel@8.6.0: resolution: {integrity: sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==} + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + enhanced-resolve@5.18.3: resolution: {integrity: sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==} engines: {node: '>=10.13.0'} @@ -2048,9 +2109,21 @@ packages: resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} engines: {node: '>=0.12'} + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + es-module-lexer@1.7.0: resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + esbuild@0.25.10: resolution: {integrity: sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==} engines: {node: '>=18'} @@ -2060,6 +2133,9 @@ packages: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + escape-string-regexp@5.0.0: resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} engines: {node: '>=12'} @@ -2070,6 +2146,10 @@ packages: estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + eventsource-parser@3.0.6: resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==} engines: {node: '>=18.0.0'} @@ -2078,6 +2158,10 @@ packages: resolution: {integrity: sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==} engines: {node: '>=12.0.0'} + express@5.1.0: + resolution: {integrity: sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==} + engines: {node: '>= 18'} + exsolve@1.0.7: resolution: {integrity: sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==} @@ -2096,10 +2180,18 @@ packages: picomatch: optional: true + finalhandler@2.1.0: + resolution: {integrity: sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==} + engines: {node: '>= 0.8'} + format@0.2.2: resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==} engines: {node: '>=0.4.x'} + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + framer-motion@12.23.24: resolution: {integrity: sha512-HMi5HRoRCTou+3fb3h9oTLyJGBxHfW+HnNE25tAXOvVx/IvwMHK0cx7IR4a2ZU6sh3IX1Z+4ts32PcYBOqka8w==} peerDependencies: @@ -2114,6 +2206,10 @@ packages: react-dom: optional: true + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + fsevents@2.3.2: resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -2124,14 +2220,25 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + get-nonce@1.0.1: resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} engines: {node: '>=6'} + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + globals@15.15.0: resolution: {integrity: sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==} engines: {node: '>=18'} @@ -2140,12 +2247,24 @@ packages: resolution: {integrity: sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==} engines: {node: '>=18'} + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} hachure-fill@0.5.2: resolution: {integrity: sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==} + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + hast-util-from-dom@5.0.1: resolution: {integrity: sha512-N+LqofjR2zuzTjCPzyDUdSshy4Ma6li7p/c3pA78uTwzFgENbgbUrm2ugwsOdcjI1muO+o6Dgzp9p8WHtn/39Q==} @@ -2210,6 +2329,10 @@ packages: html-void-elements@3.0.0: resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} + http-errors@2.0.0: + resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} + engines: {node: '>= 0.8'} + http-proxy-agent@7.0.2: resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} engines: {node: '>= 14'} @@ -2233,10 +2356,17 @@ packages: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} + iconv-lite@0.7.0: + resolution: {integrity: sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==} + engines: {node: '>=0.10.0'} + indent-string@4.0.0: resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} engines: {node: '>=8'} + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + inline-style-parser@0.2.4: resolution: {integrity: sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==} @@ -2247,6 +2377,10 @@ packages: resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} engines: {node: '>=12'} + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + is-alphabetical@1.0.4: resolution: {integrity: sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==} @@ -2278,6 +2412,9 @@ packages: is-potential-custom-element-name@1.0.1: resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + jiti@2.6.0: resolution: {integrity: sha512-VXe6RjJkBPj0ohtqaO8vSWP3ZhAKo66fKrFNCll4BTcwljPLz03pCbaNKfzGP5MbrCYcbJ7v0nOYYwUzTEIdXQ==} hasBin: true @@ -2510,6 +2647,10 @@ packages: engines: {node: '>= 20'} hasBin: true + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + mdast-util-find-and-replace@3.0.2: resolution: {integrity: sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==} @@ -2561,6 +2702,14 @@ packages: mdn-data@2.12.2: resolution: {integrity: sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==} + media-typer@1.1.0: + resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + engines: {node: '>= 0.8'} + + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + mermaid@11.12.0: resolution: {integrity: sha512-ZudVx73BwrMJfCFmSSJT84y6u5brEoV8DOItdHomNLz32uBjNrelm7mg95X7g+C6UoQH/W6mBLGDEDv73JdxBg==} @@ -2651,6 +2800,14 @@ packages: micromark@4.0.2: resolution: {integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==} + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + + mime-types@3.0.1: + resolution: {integrity: sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==} + engines: {node: '>= 0.6'} + min-indent@1.0.1: resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} engines: {node: '>=4'} @@ -2699,6 +2856,10 @@ packages: engines: {node: ^18 || >=20} hasBin: true + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + next-themes@0.4.6: resolution: {integrity: sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==} peerDependencies: @@ -2708,6 +2869,21 @@ packages: node-releases@2.0.21: resolution: {integrity: sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw==} + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + oniguruma-parser@0.12.1: resolution: {integrity: sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==} @@ -2738,9 +2914,16 @@ packages: parse5@7.3.0: resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + path-data-parser@0.1.0: resolution: {integrity: sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==} + path-to-regexp@8.3.0: + resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==} + pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} @@ -2807,13 +2990,29 @@ packages: property-information@7.1.0: resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} + qs@6.14.0: + resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} + engines: {node: '>=0.6'} + quansync@0.2.11: resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + raw-body@3.0.1: + resolution: {integrity: sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==} + engines: {node: '>= 0.10'} + react-dom@19.1.1: resolution: {integrity: sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==} peerDependencies: @@ -3005,12 +3204,19 @@ packages: roughjs@4.6.6: resolution: {integrity: sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==} + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + rrweb-cssom@0.8.0: resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==} rw@1.3.3: resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==} + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} @@ -3025,12 +3231,39 @@ packages: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true + send@1.2.0: + resolution: {integrity: sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==} + engines: {node: '>= 18'} + + serve-static@2.2.0: + resolution: {integrity: sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==} + engines: {node: '>= 18'} + set-cookie-parser@2.7.1: resolution: {integrity: sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==} + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + shiki@3.13.0: resolution: {integrity: sha512-aZW4l8Og16CokuCLf8CF8kq+KK2yOygapU5m3+hoGw0Mdosc6fPitjM+ujYarppj5ZIKGyPDPP1vqmQhr+5/0g==} + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} @@ -3053,6 +3286,14 @@ packages: stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + statuses@2.0.1: + resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} + engines: {node: '>= 0.8'} + + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + std-env@3.9.0: resolution: {integrity: sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==} @@ -3144,6 +3385,10 @@ packages: resolution: {integrity: sha512-5bdPHSwbKTeHmXrgecID4Ljff8rQjv7g8zKQPkCozRo2HWWni+p310FSn5ImI+9kWw9kK4lzOB5q/a6iv0IJsw==} hasBin: true + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + tokenlens@1.3.1: resolution: {integrity: sha512-7oxmsS5PNCX3z+b+z07hL5vCzlgHKkCGrEQjQmWl5l+v5cUrtL7S1cuST4XThaL1XyjbTX8J5hfP0cjDJRkaLA==} @@ -3185,6 +3430,10 @@ packages: tw-animate-css@1.3.8: resolution: {integrity: sha512-Qrk3PZ7l7wUcGYhwZloqfkWCmaXZAoqjkdbIDvzfGshwGtexa/DAs9koXxIkrpEasyevandomzCBAV1Yyop5rw==} + type-is@2.0.1: + resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} + engines: {node: '>= 0.6'} + typescript@5.8.3: resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} engines: {node: '>=14.17'} @@ -3220,6 +3469,10 @@ packages: unist-util-visit@5.0.0: resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==} + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + update-browserslist-db@1.1.3: resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==} hasBin: true @@ -3263,6 +3516,10 @@ packages: v8-compile-cache-lib@3.0.1: resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + vfile-location@5.0.3: resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==} @@ -3357,6 +3614,9 @@ packages: engines: {node: '>=8'} hasBin: true + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + ws@8.18.3: resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==} engines: {node: '>=10.0.0'} @@ -4819,6 +5079,11 @@ snapshots: d3-selection: 3.0.0 d3-zoom: 3.0.0 + accepts@2.0.0: + dependencies: + mime-types: 3.0.1 + negotiator: 1.0.0 + acorn-walk@8.3.4: dependencies: acorn: 8.15.0 @@ -4867,6 +5132,20 @@ snapshots: dependencies: require-from-string: 2.0.2 + body-parser@2.2.0: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 4.4.3 + http-errors: 2.0.0 + iconv-lite: 0.6.3 + on-finished: 2.4.1 + qs: 6.14.0 + raw-body: 3.0.1 + type-is: 2.0.1 + transitivePeerDependencies: + - supports-color + browserslist@4.26.2: dependencies: baseline-browser-mapping: 2.8.6 @@ -4875,8 +5154,20 @@ snapshots: node-releases: 2.0.21 update-browserslist-db: 1.1.3(browserslist@4.26.2) + bytes@3.1.2: {} + cac@6.7.14: {} + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + caniuse-lite@1.0.30001743: {} ccount@2.0.1: {} @@ -4953,10 +5244,25 @@ snapshots: confbox@0.2.2: {} + content-disposition@1.0.0: + dependencies: + safe-buffer: 5.2.1 + + content-type@1.0.5: {} + convert-source-map@2.0.0: {} + cookie-signature@1.2.2: {} + + cookie@0.7.2: {} + cookie@1.0.2: {} + cors@2.8.5: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + cose-base@1.0.3: dependencies: layout-base: 1.0.2 @@ -5191,6 +5497,8 @@ snapshots: dependencies: robust-predicates: 3.0.2 + depd@2.0.0: {} + dequal@2.0.3: {} detect-libc@2.1.0: {} @@ -5211,6 +5519,14 @@ snapshots: optionalDependencies: '@types/trusted-types': 2.0.7 + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + ee-first@1.1.1: {} + electron-to-chromium@1.5.222: {} embla-carousel-react@8.6.0(react@19.1.1): @@ -5225,6 +5541,8 @@ snapshots: embla-carousel@8.6.0: {} + encodeurl@2.0.0: {} + enhanced-resolve@5.18.3: dependencies: graceful-fs: 4.2.11 @@ -5232,8 +5550,16 @@ snapshots: entities@6.0.1: {} + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + es-module-lexer@1.7.0: {} + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + esbuild@0.25.10: optionalDependencies: '@esbuild/aix-ppc64': 0.25.10 @@ -5266,6 +5592,8 @@ snapshots: escalade@3.2.0: {} + escape-html@1.0.3: {} + escape-string-regexp@5.0.0: {} estree-util-is-identifier-name@3.0.0: {} @@ -5274,10 +5602,44 @@ snapshots: dependencies: '@types/estree': 1.0.8 + etag@1.8.1: {} + eventsource-parser@3.0.6: {} expect-type@1.2.2: {} + express@5.1.0: + dependencies: + accepts: 2.0.0 + body-parser: 2.2.0 + content-disposition: 1.0.0 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.2.2 + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.1.0 + fresh: 2.0.0 + http-errors: 2.0.0 + merge-descriptors: 2.0.0 + mime-types: 3.0.1 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.7 + qs: 6.14.0 + range-parser: 1.2.1 + router: 2.2.0 + send: 1.2.0 + serve-static: 2.2.0 + statuses: 2.0.2 + type-is: 2.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + exsolve@1.0.7: {} extend@3.0.2: {} @@ -5290,8 +5652,21 @@ snapshots: optionalDependencies: picomatch: 4.0.3 + finalhandler@2.1.0: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + format@0.2.2: {} + forwarded@0.2.0: {} + framer-motion@12.23.24(react-dom@19.1.1(react@19.1.1))(react@19.1.1): dependencies: motion-dom: 12.23.23 @@ -5301,24 +5676,54 @@ snapshots: react: 19.1.1 react-dom: 19.1.1(react@19.1.1) + fresh@2.0.0: {} + fsevents@2.3.2: optional: true fsevents@2.3.3: optional: true + function-bind@1.1.2: {} + gensync@1.0.0-beta.2: {} + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + get-nonce@1.0.1: {} + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + globals@15.15.0: {} globals@16.4.0: {} + gopd@1.2.0: {} + graceful-fs@4.2.11: {} hachure-fill@0.5.2: {} + has-symbols@1.1.0: {} + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + hast-util-from-dom@5.0.1: dependencies: '@types/hast': 3.0.4 @@ -5465,6 +5870,14 @@ snapshots: html-void-elements@3.0.0: {} + http-errors@2.0.0: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.1 + toidentifier: 1.0.1 + http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.4 @@ -5493,14 +5906,22 @@ snapshots: dependencies: safer-buffer: 2.1.2 + iconv-lite@0.7.0: + dependencies: + safer-buffer: 2.1.2 + indent-string@4.0.0: {} + inherits@2.0.4: {} + inline-style-parser@0.2.4: {} internmap@1.0.1: {} internmap@2.0.3: {} + ipaddr.js@1.9.1: {} + is-alphabetical@1.0.4: {} is-alphabetical@2.0.1: {} @@ -5527,6 +5948,8 @@ snapshots: is-potential-custom-element-name@1.0.1: {} + is-promise@4.0.0: {} + jiti@2.6.0: {} js-tokens@4.0.0: {} @@ -5720,6 +6143,8 @@ snapshots: marked@16.4.1: {} + math-intrinsics@1.1.0: {} + mdast-util-find-and-replace@3.0.2: dependencies: '@types/mdast': 4.0.4 @@ -5887,6 +6312,10 @@ snapshots: mdn-data@2.12.2: {} + media-typer@1.1.0: {} + + merge-descriptors@2.0.0: {} + mermaid@11.12.0: dependencies: '@braintree/sanitize-url': 7.1.1 @@ -6113,6 +6542,12 @@ snapshots: transitivePeerDependencies: - supports-color + mime-db@1.54.0: {} + + mime-types@3.0.1: + dependencies: + mime-db: 1.54.0 + min-indent@1.0.1: {} minipass@7.1.2: {} @@ -6148,6 +6583,8 @@ snapshots: nanoid@5.1.6: {} + negotiator@1.0.0: {} + next-themes@0.4.6(react-dom@19.1.1(react@19.1.1))(react@19.1.1): dependencies: react: 19.1.1 @@ -6155,6 +6592,18 @@ snapshots: node-releases@2.0.21: {} + object-assign@4.1.1: {} + + object-inspect@1.13.4: {} + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + oniguruma-parser@0.12.1: {} oniguruma-to-es@4.3.3: @@ -6193,8 +6642,12 @@ snapshots: dependencies: entities: 6.0.1 + parseurl@1.3.3: {} + path-data-parser@0.1.0: {} + path-to-regexp@8.3.0: {} + pathe@2.0.3: {} pathval@2.0.1: {} @@ -6260,10 +6713,28 @@ snapshots: property-information@7.1.0: {} + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + punycode@2.3.1: {} + qs@6.14.0: + dependencies: + side-channel: 1.1.0 + quansync@0.2.11: {} + range-parser@1.2.1: {} + + raw-body@3.0.1: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.0 + iconv-lite: 0.7.0 + unpipe: 1.0.0 + react-dom@19.1.1(react@19.1.1): dependencies: react: 19.1.1 @@ -6489,10 +6960,22 @@ snapshots: points-on-curve: 0.2.0 points-on-path: 0.2.1 + router@2.2.0: + dependencies: + debug: 4.4.3 + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.3.0 + transitivePeerDependencies: + - supports-color + rrweb-cssom@0.8.0: {} rw@1.3.3: {} + safe-buffer@5.2.1: {} + safer-buffer@2.1.2: {} saxes@6.0.0: @@ -6503,8 +6986,35 @@ snapshots: semver@6.3.1: {} + send@1.2.0: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.0 + mime-types: 3.0.1 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + serve-static@2.2.0: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.0 + transitivePeerDependencies: + - supports-color + set-cookie-parser@2.7.1: {} + setprototypeof@1.2.0: {} + shiki@3.13.0: dependencies: '@shikijs/core': 3.13.0 @@ -6516,6 +7026,34 @@ snapshots: '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.4 + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + siginfo@2.0.0: {} sonner@2.0.7(react-dom@19.1.1(react@19.1.1))(react@19.1.1): @@ -6531,6 +7069,10 @@ snapshots: stackback@0.0.2: {} + statuses@2.0.1: {} + + statuses@2.0.2: {} + std-env@3.9.0: {} streamdown@1.4.0(@types/react@19.1.13)(react@19.1.1): @@ -6630,6 +7172,8 @@ snapshots: dependencies: tldts-core: 7.0.16 + toidentifier@1.0.1: {} + tokenlens@1.3.1: dependencies: '@tokenlens/core': 1.3.0 @@ -6673,6 +7217,12 @@ snapshots: tw-animate-css@1.3.8: {} + type-is@2.0.1: + dependencies: + content-type: 1.0.5 + media-typer: 1.1.0 + mime-types: 3.0.1 + typescript@5.8.3: {} ufo@1.6.1: {} @@ -6722,6 +7272,8 @@ snapshots: unist-util-is: 6.0.1 unist-util-visit-parents: 6.0.2 + unpipe@1.0.0: {} + update-browserslist-db@1.1.3(browserslist@4.26.2): dependencies: browserslist: 4.26.2 @@ -6755,6 +7307,8 @@ snapshots: v8-compile-cache-lib@3.0.1: {} + vary@1.1.2: {} + vfile-location@5.0.3: dependencies: '@types/unist': 3.0.3 @@ -6877,6 +7431,8 @@ snapshots: siginfo: 2.0.0 stackback: 0.0.2 + wrappy@1.0.2: {} + ws@8.18.3: {} xml-name-validator@5.0.0: {} diff --git a/frontend_omni/tests/mock-openai-helper.ts b/frontend_omni/tests/mock-openai-helper.ts new file mode 100644 index 0000000..d91d07b --- /dev/null +++ b/frontend_omni/tests/mock-openai-helper.ts @@ -0,0 +1,121 @@ +import { APIRequestContext } from '@playwright/test'; + +const MOCK_SERVER_URL = 'http://localhost:3001'; + +/** + * Test helper for controlling the mock OpenAI server responses + */ +export class MockOpenAIHelper { + private request: APIRequestContext; + + constructor(request: APIRequestContext) { + this.request = request; + } + + /** + * Set the mock models response + */ + async setModels(models: Array<{ id: string; object: string; created: number; owned_by: string }>) { + const response = await this.request.post(`${MOCK_SERVER_URL}/admin/set-models`, { + data: { models } + }); + return response.json(); + } + + /** + * Set the mock chat completion response + */ + async setCompletionResponse(response: any) { + const apiResponse = await this.request.post(`${MOCK_SERVER_URL}/admin/set-completion-response`, { + data: { response } + }); + return apiResponse.json(); + } + + /** + * Set the mock streaming response chunks + */ + async setStreamingResponse(chunks: any[]) { + const response = await this.request.post(`${MOCK_SERVER_URL}/admin/set-streaming-response`, { + data: { chunks } + }); + return response.json(); + } + + /** + * Reset all mock responses to defaults + */ + async reset() { + const response = await this.request.post(`${MOCK_SERVER_URL}/admin/reset`); + return response.json(); + } + + /** + * Set a simple text response for chat completion + */ + async setSimpleCompletionResponse(text: string, model = 'gpt-3.5-turbo') { + const response = { + id: 'chatcmpl-mock', + object: 'chat.completion', + created: Date.now(), + model, + choices: [{ + index: 0, + message: { + role: 'assistant', + content: text + }, + finish_reason: 'stop' + }], + usage: { + prompt_tokens: 10, + completion_tokens: text.split(' ').length, + total_tokens: 10 + text.split(' ').length + } + }; + return this.setCompletionResponse(response); + } + + /** + * Set a simple streaming response + */ + async setSimpleStreamingResponse(text: string, model = 'gpt-3.5-turbo') { + const words = text.split(' '); + const chunks = words.map((word, index) => ({ + id: 'chatcmpl-mock-stream', + object: 'chat.completion.chunk', + created: Date.now(), + model, + choices: [{ + index: 0, + delta: { content: word + (index < words.length - 1 ? ' ' : '') }, + finish_reason: index === words.length - 1 ? 'stop' : null + }] + })); + return this.setStreamingResponse(chunks); + } + + /** + * Set an error response + */ + async setErrorResponse(statusCode: number, message: string) { + // For errors, we need to modify the server behavior + // This is a simplified approach - in a real implementation you might want to + // add more admin endpoints for error simulation + const errorResponse = { + error: { + message, + type: 'invalid_request_error', + code: statusCode + } + }; + return this.setCompletionResponse(errorResponse); + } +} + +/** + * Factory function to create a MockOpenAIHelper instance + */ +export function createMockOpenAIHelper(request: APIRequestContext): MockOpenAIHelper { + return new MockOpenAIHelper(request); +} diff --git a/frontend_omni/tests/mock-openai-server.spec.ts b/frontend_omni/tests/mock-openai-server.spec.ts new file mode 100644 index 0000000..0e81c22 --- /dev/null +++ b/frontend_omni/tests/mock-openai-server.spec.ts @@ -0,0 +1,103 @@ +import { expect, test } from "@playwright/test"; +import { createMockOpenAIHelper } from "./mock-openai-helper"; + +test.describe("Mock OpenAI Server Tests", () => { + test("should mock chat completion response", async ({ request }) => { + const mockHelper = createMockOpenAIHelper(request); + + // Set up mock response + await mockHelper.setSimpleCompletionResponse("Hello from mock server!"); + + // Test the mock endpoint directly + const response = await request.post("http://localhost:3001/chat/completions", { + data: { + model: "gpt-3.5-turbo", + messages: [{ role: "user", content: "Hello" }] + } + }); + + expect(response.ok()).toBeTruthy(); + const data = await response.json(); + expect(data.choices[0].message.content).toBe("Hello from mock server!"); + }); + + test("should mock models response", async ({ request }) => { + const mockHelper = createMockOpenAIHelper(request); + + // Set up custom models + await mockHelper.setModels([ + { + id: "custom-model-1", + object: "model", + created: 1234567890, + owned_by: "test" + } + ]); + + // Test the models endpoint + const response = await request.get("http://localhost:3001/models"); + expect(response.ok()).toBeTruthy(); + const data = await response.json(); + expect(data.data).toHaveLength(1); + expect(data.data[0].id).toBe("custom-model-1"); + }); + + test("should mock streaming response", async ({ request }) => { + const mockHelper = createMockOpenAIHelper(request); + + // Set up streaming response + await mockHelper.setSimpleStreamingResponse("This is a streaming response"); + + // Test streaming endpoint + const response = await request.post("http://localhost:3001/chat/completions", { + data: { + model: "gpt-3.5-turbo", + messages: [{ role: "user", content: "Hello" }], + stream: true + } + }); + + expect(response.ok()).toBeTruthy(); + const text = await response.text(); + // Check that the streaming response contains the expected content pieces + expect(text).toContain('"content":"This "'); + expect(text).toContain('"content":"is "'); + expect(text).toContain('"content":"a "'); + expect(text).toContain('"content":"streaming "'); + expect(text).toContain('"content":"response"'); + expect(text).toContain('"finish_reason":"stop"'); + expect(text).toContain('data: [DONE]'); + }); + + test("should reset mock server", async ({ request }) => { + const mockHelper = createMockOpenAIHelper(request); + + // First set custom response + const setResult = await mockHelper.setSimpleCompletionResponse("Custom response for reset test"); + expect(setResult.success).toBe(true); + + // Verify it's set + let response = await request.post("http://localhost:3001/chat/completions", { + data: { + model: "gpt-3.5-turbo", + messages: [{ role: "user", content: "Hello" }] + } + }); + let data = await response.json(); + expect(data.choices[0].message.content).toBe("Custom response for reset test"); + + // Reset + const resetResult = await mockHelper.reset(); + expect(resetResult.success).toBe(true); + + // Verify it's back to default + response = await request.post("http://localhost:3001/chat/completions", { + data: { + model: "gpt-3.5-turbo", + messages: [{ role: "user", content: "Hello" }] + } + }); + data = await response.json(); + expect(data.choices[0].message.content).toBe("This is a mock response from the test server."); + }); +}); From 09bf43fedca2bf0a0177703765601bba489643c2 Mon Sep 17 00:00:00 2001 From: guenhter Date: Thu, 23 Oct 2025 06:56:00 +0200 Subject: [PATCH 02/25] test: add llm config test --- .editorconfig | 2 +- .../src/modules/llm-picker/LLMPicker.tsx | 70 +++++--- .../LLMProviderManagementPage.tsx | 4 +- .../tests/llm-provider-management.spec.ts | 167 ++++++++++++++++++ frontend_omni/tests/mock-openai-helper.ts | 151 +++++++++++----- .../tests/mock-openai-server.spec.ts | 84 +++++---- 6 files changed, 381 insertions(+), 97 deletions(-) create mode 100644 frontend_omni/tests/llm-provider-management.spec.ts diff --git a/.editorconfig b/.editorconfig index e41734a..b473a60 100644 --- a/.editorconfig +++ b/.editorconfig @@ -8,5 +8,5 @@ insert_final_newline = true trim_trailing_whitespace = true indent_style = space -[{*.ts,*.tsx}] +[{*.ts,*.tsx,*.js}] indent_size = 4 diff --git a/frontend_omni/src/modules/llm-picker/LLMPicker.tsx b/frontend_omni/src/modules/llm-picker/LLMPicker.tsx index fac5648..1d1cb50 100644 --- a/frontend_omni/src/modules/llm-picker/LLMPicker.tsx +++ b/frontend_omni/src/modules/llm-picker/LLMPicker.tsx @@ -2,7 +2,11 @@ import { useSuspenseQuery } from "@tanstack/react-query"; import { AlertTriangle } from "lucide-react"; import { Trans, useTranslation } from "react-i18next"; import { Link } from "react-router-dom"; -import type { Model, Provider } from "@/modules/llm-provider-service"; +import type { + Model, + Provider, + ProviderService, +} from "@/modules/llm-provider-service"; import { useLLMProviderService } from "@/modules/llm-provider-service"; import { Alert, AlertDescription } from "@/shadcn/components/ui/alert"; import { @@ -31,27 +35,7 @@ export function LLMPicker() { const { data: models } = useSuspenseQuery({ queryKey: ["llm-models"], - queryFn: async () => { - const allModels: ModelOption[] = []; - for (const type of PROVIDER_TYPES) { - const providers = await service.getProviders(type); - for (const provider of providers) { - const providerModels = await service.getModels( - type, - provider.id, - ); - allModels.push( - ...providerModels.map((model) => ({ - id: `${provider.id}/${model.id}`, - name: `${provider.name} - ${model.name}`, - provider, - model, - })), - ); - } - } - return allModels; - }, + queryFn: async () => fetchAllModels(service), }); return ( @@ -116,4 +100,46 @@ export function LLMPicker() { ); } +async function fetchAllModels( + service: ProviderService, +): Promise { + const allModels: ModelOption[] = []; + for (const type of PROVIDER_TYPES) { + const providers = await service.getProviders(type); + for (const provider of providers) { + const models = await fetchModelsForProvider(service, provider); + allModels.push(...models); + } + } + return allModels; +} + +async function fetchModelsForProvider( + service: ProviderService, + provider: Provider, +): Promise { + const allModels: ModelOption[] = []; + try { + const providerModels = await service.getModels( + provider.type, + provider.id, + ); + allModels.push( + ...providerModels.map((model) => ({ + id: `${provider.id}/${model.id}`, + name: `${provider.name} - ${model.name}`, + provider, + model, + })), + ); + } catch (error) { + console.error( + `Failed to fetch models for provider '${provider.name}':`, + error, + ); + return allModels; + } + return allModels; +} + export default LLMPicker; diff --git a/frontend_omni/src/modules/llm-provider-management/LLMProviderManagementPage.tsx b/frontend_omni/src/modules/llm-provider-management/LLMProviderManagementPage.tsx index 0a87232..c46cba0 100644 --- a/frontend_omni/src/modules/llm-provider-management/LLMProviderManagementPage.tsx +++ b/frontend_omni/src/modules/llm-provider-management/LLMProviderManagementPage.tsx @@ -540,6 +540,7 @@ function NewProviderForm({ t: (key: string, options?: { defaultValue: string }) => string; }) { const nameId = useId(); + const baseUrl = useId(); const apiKeyId = useId(); if (!isAddingNew) return null; @@ -569,10 +570,11 @@ function NewProviderForm({ />
-