diff --git a/README.md b/README.md index 90bf7b7..072ca72 100644 --- a/README.md +++ b/README.md @@ -361,6 +361,9 @@ The `AI_GATEWAY_*` variables take precedence over `ANTHROPIC_*` if both are set. |--------|----------|-------------| | `AI_GATEWAY_API_KEY` | Yes* | API key for your AI Gateway provider (requires `AI_GATEWAY_BASE_URL`) | | `AI_GATEWAY_BASE_URL` | Yes* | AI Gateway endpoint URL (required when using `AI_GATEWAY_API_KEY`) | +| `AI_GATEWAY_PROVIDER` | No | Explicit provider type: `openai` or `anthropic` (auto-detected from URL if not set) | +| `AI_GATEWAY_MODEL` | No | Custom model name (e.g., `gpt-5-mini`) | +| `AI_GATEWAY_API_FORMAT` | No | OpenAI API format: `openai-completions` (default) or `openai-responses` | | `ANTHROPIC_API_KEY` | Yes* | Direct Anthropic API key (fallback if AI Gateway not configured) | | `ANTHROPIC_BASE_URL` | No | Direct Anthropic API base URL (fallback) | | `OPENAI_API_KEY` | No | OpenAI API key (alternative provider) | diff --git a/package-lock.json b/package-lock.json index 170a6f2..950f68a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -61,6 +61,7 @@ "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", @@ -432,6 +433,23 @@ "wrangler": "^4.60.0" } }, + "node_modules/@cloudflare/workerd-darwin-64": { + "version": "1.20260120.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260120.0.tgz", + "integrity": "sha512-JLHx3p5dpwz4wjVSis45YNReftttnI3ndhdMh5BUbbpdreN/g0jgxNt5Qp9tDFqEKl++N63qv+hxJiIIvSLR+Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, "node_modules/@cloudflare/workerd-darwin-arm64": { "version": "1.20260120.0", "cpu": [ @@ -447,10 +465,62 @@ "node": ">=16" } }, + "node_modules/@cloudflare/workerd-linux-64": { + "version": "1.20260120.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20260120.0.tgz", + "integrity": "sha512-O0mIfJfvU7F8N5siCoRDaVDuI12wkz2xlG4zK6/Ct7U9c9FiE0ViXNFWXFQm5PPj+qbkNRyhjUwhP+GCKTk5EQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-linux-arm64": { + "version": "1.20260120.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20260120.0.tgz", + "integrity": "sha512-aRHO/7bjxVpjZEmVVcpmhbzpN6ITbFCxuLLZSW0H9O0C0w40cDCClWSi19T87Ax/PQcYjFNT22pTewKsupkckA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-windows-64": { + "version": "1.20260120.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20260120.0.tgz", + "integrity": "sha512-ASZIz1E8sqZQqQCgcfY1PJbBpUDrxPt8NZ+lqNil0qxnO4qX38hbCsdDF2/TDAuq0Txh7nu8ztgTelfNDlb4EA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16" + } + }, "node_modules/@cloudflare/workers-types": { "version": "4.20260124.0", "dev": true, - "license": "MIT OR Apache-2.0" + "license": "MIT OR Apache-2.0", + "peer": true }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", @@ -463,6 +533,17 @@ "node": ">=12" } }, + "node_modules/@emnapi/runtime": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", + "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.12", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", @@ -932,6 +1013,29 @@ "@img/sharp-libvips-darwin-arm64": "1.2.4" } }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, "node_modules/@img/sharp-libvips-darwin-arm64": { "version": "1.2.4", "cpu": [ @@ -947,6 +1051,423 @@ "url": "https://opencollective.com/libvips" } }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -1526,6 +2047,7 @@ "integrity": "sha512-Lpo8kgb/igvMIPeNV2rsYKTgaORYdO1XGVZ4Qz3akwOj0ySGYMPlQWa8BaLn0G63D1aSaAQ5ldR06wCpChQCjA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -1953,6 +2475,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -3201,6 +3724,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -3290,6 +3814,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -3671,6 +4196,7 @@ "version": "2.0.0-rc.24", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "pathe": "^2.0.3" } @@ -3712,6 +4238,7 @@ "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -3846,6 +4373,7 @@ "integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@vitest/expect": "4.0.18", "@vitest/mocker": "4.0.18", @@ -3940,6 +4468,7 @@ "dev": true, "hasInstallScript": true, "license": "Apache-2.0", + "peer": true, "bin": { "workerd": "bin/workerd" }, diff --git a/src/auth/jwt.test.ts b/src/auth/jwt.test.ts index 8a5fc93..9a17125 100644 --- a/src/auth/jwt.test.ts +++ b/src/auth/jwt.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { verifyAccessJWT } from './jwt'; +import { verifyAccessJWT, normalizeTeamDomain } from './jwt'; // Mock the jose module vi.mock('jose', () => ({ @@ -153,3 +153,21 @@ describe('verifyAccessJWT', () => { expect(result.name).toBe('Test User'); }); }); + +describe('normalizeTeamDomain', () => { + it('adds cloudflareaccess.com suffix for short team names', () => { + expect(normalizeTeamDomain('team')).toBe('team.cloudflareaccess.com'); + }); + + it('keeps full domains unchanged', () => { + expect(normalizeTeamDomain('team.cloudflareaccess.com')).toBe('team.cloudflareaccess.com'); + }); + + it('strips https:// prefix', () => { + expect(normalizeTeamDomain('https://team.cloudflareaccess.com')).toBe('team.cloudflareaccess.com'); + }); + + it('strips trailing slash and adds suffix', () => { + expect(normalizeTeamDomain('team/')).toBe('team.cloudflareaccess.com'); + }); +}); diff --git a/src/auth/jwt.ts b/src/auth/jwt.ts index 74dcb52..2447ede 100644 --- a/src/auth/jwt.ts +++ b/src/auth/jwt.ts @@ -1,6 +1,27 @@ import { jwtVerify, createRemoteJWKSet, type JWTPayload as JoseJWTPayload } from 'jose'; import type { JWTPayload } from '../types'; +/** + * Normalize a Cloudflare Access team domain. + * Handles cases where user provides just the team name or the full domain. + * + * @param teamDomain - Team domain (e.g., 'myteam' or 'myteam.cloudflareaccess.com') + * @returns Normalized domain (e.g., 'myteam.cloudflareaccess.com') + */ +export function normalizeTeamDomain(teamDomain: string): string { + // Remove https:// if present and trailing slashes + let domain = teamDomain + .replace(/^https?:\/\//, '') + .replace(/\/+$/, ''); + + // If it's just a team name (no dots), add the Access domain suffix + if (!domain.includes('.')) { + domain = `${domain}.cloudflareaccess.com`; + } + + return domain; +} + /** * Verify a Cloudflare Access JWT token using the jose library. * @@ -18,10 +39,13 @@ export async function verifyAccessJWT( teamDomain: string, expectedAud: string ): Promise { + // Normalize team domain + const normalizedDomain = normalizeTeamDomain(teamDomain); + // Ensure teamDomain has https:// prefix for issuer check - const issuer = teamDomain.startsWith('https://') - ? teamDomain - : `https://${teamDomain}`; + const issuer = normalizedDomain.startsWith('https://') + ? normalizedDomain + : `https://${normalizedDomain}`; // Create JWKS from the team domain const JWKS = createRemoteJWKSet(new URL(`${issuer}/cdn-cgi/access/certs`)); diff --git a/src/auth/middleware.test.ts b/src/auth/middleware.test.ts index 1c49ce6..d1e8fc9 100644 --- a/src/auth/middleware.test.ts +++ b/src/auth/middleware.test.ts @@ -70,8 +70,8 @@ describe('extractJWT', () => { it('extracts JWT from CF_Authorization cookie with other cookies', () => { const jwt = 'cookie.payload.signature'; - const c = createMockContext({ - cookies: `other=value; CF_Authorization=${jwt}; another=test` + const c = createMockContext({ + cookies: `other=value; CF_Authorization=${jwt}; another=test` }); expect(extractJWT(c)).toBe(jwt); }); @@ -79,9 +79,9 @@ describe('extractJWT', () => { it('prefers header over cookie', () => { const headerJwt = 'header.jwt.token'; const cookieJwt = 'cookie.jwt.token'; - const c = createMockContext({ + const c = createMockContext({ jwtHeader: headerJwt, - cookies: `CF_Authorization=${cookieJwt}` + cookies: `CF_Authorization=${cookieJwt}` }); expect(extractJWT(c)).toBe(headerJwt); }); @@ -187,8 +187,8 @@ describe('createAccessMiddleware', () => { }); it('returns 401 JSON error when JWT is missing', async () => { - const { c, jsonMock } = createFullMockContext({ - env: { CF_ACCESS_TEAM_DOMAIN: 'team.cloudflareaccess.com', CF_ACCESS_AUD: 'aud123' } + const { c, jsonMock } = createFullMockContext({ + env: { CF_ACCESS_TEAM_DOMAIN: 'team', CF_ACCESS_AUD: 'aud123' } }); const middleware = createAccessMiddleware({ type: 'json' }); const next = vi.fn(); @@ -203,8 +203,8 @@ describe('createAccessMiddleware', () => { }); it('returns 401 HTML error when JWT is missing', async () => { - const { c, htmlMock } = createFullMockContext({ - env: { CF_ACCESS_TEAM_DOMAIN: 'team.cloudflareaccess.com', CF_ACCESS_AUD: 'aud123' } + const { c, htmlMock } = createFullMockContext({ + env: { CF_ACCESS_TEAM_DOMAIN: 'team', CF_ACCESS_AUD: 'aud123' } }); const middleware = createAccessMiddleware({ type: 'html' }); const next = vi.fn(); @@ -219,8 +219,8 @@ describe('createAccessMiddleware', () => { }); it('redirects when JWT is missing and redirectOnMissing is true', async () => { - const { c, redirectMock } = createFullMockContext({ - env: { CF_ACCESS_TEAM_DOMAIN: 'team.cloudflareaccess.com', CF_ACCESS_AUD: 'aud123' } + const { c, redirectMock } = createFullMockContext({ + env: { CF_ACCESS_TEAM_DOMAIN: 'team', CF_ACCESS_AUD: 'aud123' } }); const middleware = createAccessMiddleware({ type: 'html', redirectOnMissing: true }); const next = vi.fn(); diff --git a/src/auth/middleware.ts b/src/auth/middleware.ts index a1b7d22..4f04f10 100644 --- a/src/auth/middleware.ts +++ b/src/auth/middleware.ts @@ -1,6 +1,6 @@ import type { Context, Next } from 'hono'; import type { AppEnv, MoltbotEnv } from '../types'; -import { verifyAccessJWT } from './jwt'; +import { verifyAccessJWT, normalizeTeamDomain } from './jwt'; /** * Options for creating an access middleware @@ -34,7 +34,7 @@ export function extractJWT(c: Context): string | null { /** * Create a Cloudflare Access authentication middleware - * + * * @param options - Middleware options * @returns Hono middleware function */ @@ -75,9 +75,9 @@ export function createAccessMiddleware(options: AccessMiddlewareOptions) { if (!jwt) { if (type === 'html' && redirectOnMissing) { - return c.redirect(`https://${teamDomain}`, 302); + return c.redirect(`https://${normalizeTeamDomain(teamDomain)}`, 302); } - + if (type === 'json') { return c.json({ error: 'Unauthorized', @@ -89,7 +89,7 @@ export function createAccessMiddleware(options: AccessMiddlewareOptions) {

Unauthorized

Missing Cloudflare Access token.

- Login + Login `, 401); @@ -103,7 +103,7 @@ export function createAccessMiddleware(options: AccessMiddlewareOptions) { await next(); } catch (err) { console.error('Access JWT verification failed:', err); - + if (type === 'json') { return c.json({ error: 'Unauthorized', @@ -115,7 +115,7 @@ export function createAccessMiddleware(options: AccessMiddlewareOptions) {

Unauthorized

Your Cloudflare Access session is invalid or expired.

- Login again + Login again `, 401); diff --git a/src/gateway/env.test.ts b/src/gateway/env.test.ts index 29f033d..a587c1d 100644 --- a/src/gateway/env.test.ts +++ b/src/gateway/env.test.ts @@ -101,7 +101,7 @@ describe('buildEnvVars', () => { SLACK_APP_TOKEN: 'slack-app', }); const result = buildEnvVars(env); - + expect(result.TELEGRAM_BOT_TOKEN).toBe('tg-token'); expect(result.TELEGRAM_DM_POLICY).toBe('pairing'); expect(result.DISCORD_BOT_TOKEN).toBe('discord-token'); @@ -116,7 +116,7 @@ describe('buildEnvVars', () => { CLAWDBOT_BIND_MODE: 'lan', }); const result = buildEnvVars(env); - + expect(result.CLAWDBOT_DEV_MODE).toBe('true'); expect(result.CLAWDBOT_BIND_MODE).toBe('lan'); }); @@ -128,7 +128,7 @@ describe('buildEnvVars', () => { TELEGRAM_BOT_TOKEN: 'tg', }); const result = buildEnvVars(env); - + expect(result).toEqual({ ANTHROPIC_API_KEY: 'sk-key', CLAWDBOT_GATEWAY_TOKEN: 'token', @@ -170,4 +170,93 @@ describe('buildEnvVars', () => { expect(result.OPENAI_BASE_URL).toBe('https://gateway.ai.cloudflare.com/v1/123/my-gw/openai'); expect(result.AI_GATEWAY_BASE_URL).toBe('https://gateway.ai.cloudflare.com/v1/123/my-gw/openai'); }); + + it('AI_GATEWAY_PROVIDER=openai overrides URL suffix detection', () => { + const env = createMockEnv({ + AI_GATEWAY_API_KEY: 'sk-gateway-key', + AI_GATEWAY_BASE_URL: 'https://gateway.example.com/anthropic', + AI_GATEWAY_PROVIDER: 'openai', + }); + const result = buildEnvVars(env); + expect(result.OPENAI_API_KEY).toBe('sk-gateway-key'); + expect(result.OPENAI_BASE_URL).toBe('https://gateway.example.com/anthropic'); + expect(result.ANTHROPIC_API_KEY).toBeUndefined(); + expect(result.AI_GATEWAY_PROVIDER).toBe('openai'); + }); + + it('AI_GATEWAY_PROVIDER=anthropic overrides URL suffix detection', () => { + const env = createMockEnv({ + AI_GATEWAY_API_KEY: 'sk-gateway-key', + AI_GATEWAY_BASE_URL: 'https://gateway.example.com/openai', + AI_GATEWAY_PROVIDER: 'anthropic', + }); + const result = buildEnvVars(env); + expect(result.ANTHROPIC_API_KEY).toBe('sk-gateway-key'); + expect(result.ANTHROPIC_BASE_URL).toBe('https://gateway.example.com/openai'); + expect(result.OPENAI_API_KEY).toBeUndefined(); + expect(result.AI_GATEWAY_PROVIDER).toBe('anthropic'); + }); + + it('passes AI_GATEWAY_MODEL through to container env vars', () => { + const env = createMockEnv({ + AI_GATEWAY_MODEL: 'claude-3-opus-20240229', + }); + const result = buildEnvVars(env); + expect(result.AI_GATEWAY_MODEL).toBe('claude-3-opus-20240229'); + }); + + it('includes both AI_GATEWAY_PROVIDER and AI_GATEWAY_MODEL when set together', () => { + const env = createMockEnv({ + AI_GATEWAY_API_KEY: 'sk-gateway-key', + AI_GATEWAY_BASE_URL: 'https://gateway.example.com/v1', + AI_GATEWAY_PROVIDER: 'openai', + AI_GATEWAY_MODEL: 'gpt-5-mini', + }); + const result = buildEnvVars(env); + expect(result.OPENAI_API_KEY).toBe('sk-gateway-key'); + expect(result.OPENAI_BASE_URL).toBe('https://gateway.example.com/v1'); + expect(result.AI_GATEWAY_PROVIDER).toBe('openai'); + expect(result.AI_GATEWAY_MODEL).toBe('gpt-5-mini'); + }); + + it('falls back to URL detection when AI_GATEWAY_PROVIDER is invalid', () => { + const env = createMockEnv({ + AI_GATEWAY_API_KEY: 'key', + AI_GATEWAY_BASE_URL: 'https://gateway.com/openai', + AI_GATEWAY_PROVIDER: 'invalid' as any, // Invalid value + }); + const result = buildEnvVars(env); + // Should fall back to URL detection, which sees /openai suffix + expect(result.OPENAI_API_KEY).toBe('key'); + }); + + it('does not pass empty AI_GATEWAY_MODEL', () => { + const env = createMockEnv({ + AI_GATEWAY_API_KEY: 'key', + AI_GATEWAY_BASE_URL: 'https://gateway.com/', + AI_GATEWAY_MODEL: '', + }); + const result = buildEnvVars(env); + expect(result.AI_GATEWAY_MODEL).toBeUndefined(); + }); + + it('passes through invalid URL format with warning', () => { + const env = createMockEnv({ + AI_GATEWAY_API_KEY: 'key', + AI_GATEWAY_BASE_URL: 'not-a-valid-url', + }); + const result = buildEnvVars(env); + // URL is still passed (validation is warning-only) + expect(result.AI_GATEWAY_BASE_URL).toBe('not-a-valid-url'); + }); + + it('passes AI_GATEWAY_API_FORMAT through to container env vars', () => { + const env = createMockEnv({ + AI_GATEWAY_API_KEY: 'key', + AI_GATEWAY_BASE_URL: 'https://gateway.com/', + AI_GATEWAY_API_FORMAT: 'openai-completions', + }); + const result = buildEnvVars(env); + expect(result.AI_GATEWAY_API_FORMAT).toBe('openai-completions'); + }); }); diff --git a/src/gateway/env.ts b/src/gateway/env.ts index a57e781..e1bd67f 100644 --- a/src/gateway/env.ts +++ b/src/gateway/env.ts @@ -1,8 +1,33 @@ import type { MoltbotEnv } from '../types'; +// Valid AI Gateway provider values +const VALID_PROVIDERS = ['openai', 'anthropic'] as const; +type ValidProvider = typeof VALID_PROVIDERS[number]; + +// Valid AI Gateway API format values +const VALID_API_FORMATS = ['openai-completions', 'openai-responses'] as const; +type ValidApiFormat = typeof VALID_API_FORMATS[number]; + +function isValidProvider(value: unknown): value is ValidProvider { + return typeof value === 'string' && VALID_PROVIDERS.includes(value as ValidProvider); +} + +function isValidApiFormat(value: unknown): value is ValidApiFormat { + return typeof value === 'string' && VALID_API_FORMATS.includes(value as ValidApiFormat); +} + +function isValidGatewayUrl(url: string): boolean { + try { + const parsed = new URL(url); + return parsed.protocol === 'https:' || parsed.protocol === 'http:'; + } catch { + return false; + } +} + /** * Build environment variables to pass to the Moltbot container process - * + * * @param env - Worker environment bindings * @returns Environment variables record */ @@ -11,7 +36,21 @@ export function buildEnvVars(env: MoltbotEnv): Record { // Normalize the base URL by removing trailing slashes const normalizedBaseUrl = env.AI_GATEWAY_BASE_URL?.replace(/\/+$/, ''); - const isOpenAIGateway = normalizedBaseUrl?.endsWith('/openai'); + + // Validate provider if explicitly set + if (env.AI_GATEWAY_PROVIDER && !isValidProvider(env.AI_GATEWAY_PROVIDER)) { + console.warn(`Invalid AI_GATEWAY_PROVIDER: ${env.AI_GATEWAY_PROVIDER}, falling back to URL detection`); + } + + // Validate gateway URL if set + if (normalizedBaseUrl && !isValidGatewayUrl(normalizedBaseUrl)) { + console.warn(`Invalid AI_GATEWAY_BASE_URL format: ${normalizedBaseUrl}`); + } + + // Detect provider type - explicit setting takes precedence over URL suffix detection + const isOpenAIGateway = isValidProvider(env.AI_GATEWAY_PROVIDER) + ? env.AI_GATEWAY_PROVIDER === 'openai' + : normalizedBaseUrl?.endsWith('/openai') ?? false; // AI Gateway vars take precedence // Map to the appropriate provider env var based on the gateway endpoint @@ -43,6 +82,16 @@ export function buildEnvVars(env: MoltbotEnv): Record { } else if (env.ANTHROPIC_BASE_URL) { envVars.ANTHROPIC_BASE_URL = env.ANTHROPIC_BASE_URL; } + // Pass explicit provider type, model, and API format overrides to container + if (env.AI_GATEWAY_PROVIDER) envVars.AI_GATEWAY_PROVIDER = env.AI_GATEWAY_PROVIDER; + if (env.AI_GATEWAY_MODEL) envVars.AI_GATEWAY_MODEL = env.AI_GATEWAY_MODEL; + // Validate API format if explicitly set + if (env.AI_GATEWAY_API_FORMAT) { + if (!isValidApiFormat(env.AI_GATEWAY_API_FORMAT)) { + console.warn(`Invalid AI_GATEWAY_API_FORMAT: ${env.AI_GATEWAY_API_FORMAT}, valid values are: ${VALID_API_FORMATS.join(', ')}`); + } + envVars.AI_GATEWAY_API_FORMAT = env.AI_GATEWAY_API_FORMAT; + } // Map MOLTBOT_GATEWAY_TOKEN to CLAWDBOT_GATEWAY_TOKEN (container expects this name) if (env.MOLTBOT_GATEWAY_TOKEN) envVars.CLAWDBOT_GATEWAY_TOKEN = env.MOLTBOT_GATEWAY_TOKEN; if (env.DEV_MODE) envVars.CLAWDBOT_DEV_MODE = env.DEV_MODE; // Pass DEV_MODE as CLAWDBOT_DEV_MODE to container diff --git a/src/routes/debug.ts b/src/routes/debug.ts index 612eb6f..a7a6c01 100644 --- a/src/routes/debug.ts +++ b/src/routes/debug.ts @@ -374,6 +374,28 @@ debug.get('/container-config', async (c) => { let config = null; try { config = JSON.parse(stdout); + // Redact sensitive fields before returning + if (config?.models?.providers?.anthropic?.apiKey) { + config.models.providers.anthropic.apiKey = '[REDACTED]'; + } + if (config?.models?.providers?.openai?.apiKey) { + config.models.providers.openai.apiKey = '[REDACTED]'; + } + if (config?.gateway?.auth?.token) { + config.gateway.auth.token = '[REDACTED]'; + } + if (config?.channels?.telegram?.botToken) { + config.channels.telegram.botToken = '[REDACTED]'; + } + if (config?.channels?.discord?.token) { + config.channels.discord.token = '[REDACTED]'; + } + if (config?.channels?.slack?.botToken) { + config.channels.slack.botToken = '[REDACTED]'; + } + if (config?.channels?.slack?.appToken) { + config.channels.slack.appToken = '[REDACTED]'; + } } catch { // Not valid JSON } diff --git a/src/types.ts b/src/types.ts index bb82c8c..7cfde45 100644 --- a/src/types.ts +++ b/src/types.ts @@ -10,6 +10,9 @@ export interface MoltbotEnv { // AI Gateway configuration (preferred) AI_GATEWAY_API_KEY?: string; // API key for the provider configured in AI Gateway AI_GATEWAY_BASE_URL?: string; // AI Gateway URL (e.g., https://gateway.ai.cloudflare.com/v1/{account_id}/{gateway_id}/anthropic) + AI_GATEWAY_PROVIDER?: 'openai' | 'anthropic'; // Explicit provider type override + AI_GATEWAY_MODEL?: string; // Custom model name override + AI_GATEWAY_API_FORMAT?: 'openai-completions' | 'openai-responses'; // OpenAI API format (default: openai-completions) // Legacy direct provider configuration (fallback) ANTHROPIC_API_KEY?: string; ANTHROPIC_BASE_URL?: string; diff --git a/start-moltbot.sh b/start-moltbot.sh index 7e225e8..8eceaa2 100644 --- a/start-moltbot.sh +++ b/start-moltbot.sh @@ -39,30 +39,30 @@ mkdir -p "$CONFIG_DIR" should_restore_from_r2() { local R2_SYNC_FILE="$BACKUP_DIR/.last-sync" local LOCAL_SYNC_FILE="$CONFIG_DIR/.last-sync" - + # If no R2 sync timestamp, don't restore if [ ! -f "$R2_SYNC_FILE" ]; then echo "No R2 sync timestamp found, skipping restore" return 1 fi - + # If no local sync timestamp, restore from R2 if [ ! -f "$LOCAL_SYNC_FILE" ]; then echo "No local sync timestamp, will restore from R2" return 0 fi - + # Compare timestamps R2_TIME=$(cat "$R2_SYNC_FILE" 2>/dev/null) LOCAL_TIME=$(cat "$LOCAL_SYNC_FILE" 2>/dev/null) - + echo "R2 last sync: $R2_TIME" echo "Local last sync: $LOCAL_TIME" - + # Convert to epoch seconds for comparison R2_EPOCH=$(date -d "$R2_TIME" +%s 2>/dev/null || echo "0") LOCAL_EPOCH=$(date -d "$LOCAL_TIME" +%s 2>/dev/null || echo "0") - + if [ "$R2_EPOCH" -gt "$LOCAL_EPOCH" ]; then echo "R2 backup is newer, will restore" return 0 @@ -213,7 +213,9 @@ if (process.env.SLACK_BOT_TOKEN && process.env.SLACK_APP_TOKEN) { // https://gateway.ai.cloudflare.com/v1/{account_id}/{gateway_id}/anthropic // https://gateway.ai.cloudflare.com/v1/{account_id}/{gateway_id}/openai const baseUrl = (process.env.AI_GATEWAY_BASE_URL || process.env.ANTHROPIC_BASE_URL || '').replace(/\/+$/, ''); -const isOpenAI = baseUrl.endsWith('/openai'); +const provider = process.env.AI_GATEWAY_PROVIDER || ''; +const customModel = process.env.AI_GATEWAY_MODEL || ''; +const isOpenAI = provider === 'openai' || (!provider && baseUrl.endsWith('/openai')); if (isOpenAI) { // Create custom openai provider config with baseUrl override @@ -221,21 +223,36 @@ if (isOpenAI) { console.log('Configuring OpenAI provider with base URL:', baseUrl); config.models = config.models || {}; config.models.providers = config.models.providers || {}; + + // Use custom model if specified, otherwise use defaults + const defaultModels = [ + { id: 'gpt-5.2', name: 'GPT-5.2', contextWindow: 200000 }, + { id: 'gpt-5', name: 'GPT-5', contextWindow: 200000 }, + { id: 'gpt-4.5-preview', name: 'GPT-4.5 Preview', contextWindow: 128000 }, + ]; + const models = customModel + ? [{ id: customModel, name: customModel, contextWindow: 200000 }, ...defaultModels] + : defaultModels; + const primaryModel = customModel || 'gpt-5.2'; + config.models.providers.openai = { baseUrl: baseUrl, - api: 'openai-responses', - models: [ - { id: 'gpt-5.2', name: 'GPT-5.2', contextWindow: 200000 }, - { id: 'gpt-5', name: 'GPT-5', contextWindow: 200000 }, - { id: 'gpt-4.5-preview', name: 'GPT-4.5 Preview', contextWindow: 128000 }, - ] + api: process.env.AI_GATEWAY_API_FORMAT || 'openai-completions', + models: models }; + // Include API key in provider config if set (required when using custom baseUrl) + if (process.env.OPENAI_API_KEY) { + config.models.providers.openai.apiKey = process.env.OPENAI_API_KEY; + } // Add models to the allowlist so they appear in /models config.agents.defaults.models = config.agents.defaults.models || {}; + if (customModel) { + config.agents.defaults.models['openai/' + customModel] = { alias: customModel }; + } config.agents.defaults.models['openai/gpt-5.2'] = { alias: 'GPT-5.2' }; config.agents.defaults.models['openai/gpt-5'] = { alias: 'GPT-5' }; config.agents.defaults.models['openai/gpt-4.5-preview'] = { alias: 'GPT-4.5' }; - config.agents.defaults.model.primary = 'openai/gpt-5.2'; + config.agents.defaults.model.primary = 'openai/' + primaryModel; } else if (baseUrl) { console.log('Configuring Anthropic provider with base URL:', baseUrl); config.models = config.models || {}; @@ -268,7 +285,23 @@ if (isOpenAI) { // Write updated config fs.writeFileSync(configPath, JSON.stringify(config, null, 2)); console.log('Configuration updated successfully'); -console.log('Config:', JSON.stringify(config, null, 2)); + +// Redact sensitive fields before logging +const safeConfig = JSON.parse(JSON.stringify(config)); +if (safeConfig.models?.providers?.anthropic?.apiKey) { + safeConfig.models.providers.anthropic.apiKey = '[REDACTED]'; +} +if (safeConfig.models?.providers?.openai?.apiKey) { + safeConfig.models.providers.openai.apiKey = '[REDACTED]'; +} +if (safeConfig.gateway?.auth?.token) { + safeConfig.gateway.auth.token = '[REDACTED]'; +} +// Remove channels entirely to avoid logging chat bot tokens +if (safeConfig.channels) { + delete safeConfig.channels; +} +console.log('Config (redacted):', JSON.stringify(safeConfig, null, 2)); EOFNODE # ============================================================