├── .github └── workflows │ └── dockertag.yaml ├── .gitignore ├── Dockerfile ├── README.md ├── package-lock.json ├── package.json ├── src ├── functions.ts ├── functions │ ├── describe-model.ts │ ├── execute-model.ts │ ├── list-models.ts │ └── recommend-model.ts ├── index.ts └── models-api.ts └── tsconfig.json /.github/workflows/dockertag.yaml: -------------------------------------------------------------------------------- 1 | name: Create and publish a Docker image 2 | on: 3 | push: 4 | branches: 5 | - main 6 | tags: 7 | - "v*" 8 | env: 9 | REGISTRY: ghcr.io 10 | IMAGE_NAME: ${{ github.repository }} 11 | jobs: 12 | build-and-push-image: 13 | runs-on: ubuntu-latest 14 | permissions: 15 | contents: read 16 | packages: write 17 | attestations: write 18 | id-token: write 19 | steps: 20 | - name: Checkout repository 21 | uses: actions/checkout@v4 22 | - name: Log in to the Container registry 23 | uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 24 | with: 25 | registry: ${{ env.REGISTRY }} 26 | username: ${{ github.actor }} 27 | password: ${{ secrets.GITHUB_TOKEN }} 28 | # This step uses [docker/metadata-action](https://github.com/docker/metadata-action#about) to extract tags and labels that will be applied to the specified image. The `id` "meta" allows the output of this step to be referenced in a subsequent step. The `images` value provides the base name for the tags and labels. 29 | - name: Extract metadata (tags, labels) for Docker 30 | id: meta 31 | uses: docker/metadata-action@8e5442c4ef9f78752691e2d8f8d19755c6f78e81 32 | with: 33 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 34 | tags: | 35 | type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'main') }} 36 | type=sha,format=long 37 | type=ref,event=tag 38 | # This step uses the `docker/build-push-action` action to build the image, based on your repository's `Dockerfile`. If the build succeeds, it pushes the image to GitHub Packages. 39 | # It uses the `context` parameter to define the build's context as the set of files located in the specified path. For more information, see "[Usage](https://github.com/docker/build-push-action#usage)" in the README of the `docker/build-push-action` repository. 40 | # It uses the `tags` and `labels` parameters to tag and label the image with the output from the "meta" step. 41 | - name: Build and push Docker image 42 | id: push 43 | uses: docker/build-push-action@5cd11c3a4ced054e52742c5fd54dca954e0edd85 44 | with: 45 | context: . 46 | push: true 47 | tags: ${{ steps.meta.outputs.tags }} 48 | labels: ${{ steps.meta.outputs.labels }} 49 | 50 | # This step generates an artifact attestation for the image, which is an unforgeable statement about where and how it was built. It increases supply chain security for people who consume the image. For more information, see "[AUTOTITLE](/actions/security-guides/using-artifact-attestations-to-establish-provenance-for-builds)." 51 | - name: Generate artifact attestation 52 | uses: actions/attest-build-provenance@310b0a4a3b0b78ef57ecda988ee04b132db73ef8 53 | with: 54 | subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}} 55 | subject-digest: ${{ steps.push.outputs.digest }} 56 | push-to-registry: true 57 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | 132 | ./dist -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:20.16.0-alpine AS builder 2 | WORKDIR /usr/src/app 3 | COPY package*.json ./ 4 | RUN npm ci 5 | COPY . . 6 | RUN npm run build 7 | 8 | FROM node:20.16.0-alpine 9 | ENV NODE_ENV=production 10 | USER node 11 | WORKDIR /usr/src/app 12 | COPY package*.json ./ 13 | RUN npm ci --production 14 | COPY --from=builder /usr/src/app/dist ./dist 15 | 16 | ENV PORT=8080 17 | EXPOSE 8080 18 | CMD [ "node", "dist/index.js" ] 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @models Copilot Extension 2 | 3 | This is an agent-based [GitHub Copilot Extension](https://docs.github.com/en/copilot/using-github-copilot/using-extensions-to-integrate-external-tools-with-copilot-chat) that provides a way to interact with [GitHub Models](https://github.blog/news-insights/product-news/introducing-github-models/), directly in Copilot Chat. 4 | 5 | ## What it can do 6 | 7 | You can ask it things like "what models are available?" or "which model should I use for my use-case?". You can also ask it to execute a basic prompt using a particular model. 8 | 9 | | Description | Image | 10 | | --- |--- | 11 | | User asking `@models` for a recommended model that is low-cost and supports function calling | ![User asking @models for a recommended model that is low-cost and supports function calling](https://github.com/user-attachments/assets/b0c2710d-3d3e-46af-a021-864a17fb5a9c) | 12 | | Dialogue with the Extension, asking for an OpenAI model | ![Dialogue with the Extension, asking for an OpenAI model](https://github.com/user-attachments/assets/1d042c46-895f-43f7-9ab4-2f261811c6b1) | 13 | | User executing a basic prompt using the model that the extension recommended | ![User executing a basic prompt using the model that the extension recommended](https://github.com/user-attachments/assets/688951dc-c02d-40df-a226-c5da8900b633) | 14 | 15 | ## Development 16 | 17 | 1. Install dependencies: 18 | 19 | ```bash 20 | npm install 21 | ``` 22 | 23 | 2. Run the server 24 | 25 | - To run in development mode: 26 | 27 | ```bash 28 | npm run dev 29 | ``` 30 | 31 | - To build and run in production mode: 32 | 33 | ```bash 34 | npm run build && npm start 35 | ``` 36 | 3. Follow [this guide](https://docs.github.com/en/copilot/building-copilot-extensions/creating-a-copilot-extension/configuring-your-server-to-deploy-your-copilot-agent#configuring-your-server) to make your server accessible to the internet 37 | 38 | In short, we would expose a public URL for our local server using the following command (follow the guide for detailed setup instructions): 39 | ``` 40 | ngrok http http://localhost:3000 41 | ``` 42 | 43 | 4. Follow [this guide](https://docs.github.com/en/copilot/building-copilot-extensions/creating-a-copilot-extension/creating-a-github-app-for-your-copilot-extension) to create a GitHub app 44 | 5. Follow [this guide](https://docs.github.com/en/copilot/building-copilot-extensions/creating-a-copilot-extension/configuring-your-github-app-for-your-copilot-agent) to configure the app to use the public URL from (3.) 45 | 6. Use your newly installed app! On any copilot enabled page, type `@your-app-name ` (from the app created in 4.) to interact with your local installation of this extension! 46 | 47 | ## Copilot Extensions Documentation 48 | - [Using Copilot Extensions](https://docs.github.com/en/copilot/using-github-copilot/using-extensions-to-integrate-external-tools-with-copilot-chat) 49 | - [About building Copilot Extensions](https://docs.github.com/en/copilot/building-copilot-extensions/about-building-copilot-extensions) 50 | - [Set up process](https://docs.github.com/en/copilot/building-copilot-extensions/setting-up-copilot-extensions) 51 | - [Communicating with the Copilot platform](https://docs.github.com/en/copilot/building-copilot-extensions/building-a-copilot-agent-for-your-copilot-extension/configuring-your-copilot-agent-to-communicate-with-the-copilot-platform) 52 | - [Communicating with GitHub](https://docs.github.com/en/copilot/building-copilot-extensions/building-a-copilot-agent-for-your-copilot-extension/configuring-your-copilot-agent-to-communicate-with-github) 53 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gh-models", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "gh-models", 9 | "version": "1.0.0", 10 | "license": "ISC", 11 | "dependencies": { 12 | "@copilot-extensions/preview-sdk": "^5.0.1", 13 | "openai": "^4.55.0" 14 | }, 15 | "devDependencies": { 16 | "@types/express": "^4.17.21", 17 | "@types/node": "^22.1.0", 18 | "tsx": "^4.19.3", 19 | "typescript": "^5.5.4" 20 | } 21 | }, 22 | "node_modules/@copilot-extensions/preview-sdk": { 23 | "version": "5.0.1", 24 | "resolved": "https://registry.npmjs.org/@copilot-extensions/preview-sdk/-/preview-sdk-5.0.1.tgz", 25 | "integrity": "sha512-DvuJtTBl0aWvZnaNYVBiXuCdJN27ya6GYcJjv6J42Pnu1ubXHm8GHzUhIgLU/35c87HVHaITVah/jXogH3eChg==", 26 | "license": "MIT", 27 | "dependencies": { 28 | "@octokit/request": "^9.1.3", 29 | "@octokit/request-error": "^6.1.4" 30 | } 31 | }, 32 | "node_modules/@esbuild/aix-ppc64": { 33 | "version": "0.25.2", 34 | "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.2.tgz", 35 | "integrity": "sha512-wCIboOL2yXZym2cgm6mlA742s9QeJ8DjGVaL39dLN4rRwrOgOyYSnOaFPhKZGLb2ngj4EyfAFjsNJwPXZvseag==", 36 | "cpu": [ 37 | "ppc64" 38 | ], 39 | "dev": true, 40 | "license": "MIT", 41 | "optional": true, 42 | "os": [ 43 | "aix" 44 | ], 45 | "engines": { 46 | "node": ">=18" 47 | } 48 | }, 49 | "node_modules/@esbuild/android-arm": { 50 | "version": "0.25.2", 51 | "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.2.tgz", 52 | "integrity": "sha512-NQhH7jFstVY5x8CKbcfa166GoV0EFkaPkCKBQkdPJFvo5u+nGXLEH/ooniLb3QI8Fk58YAx7nsPLozUWfCBOJA==", 53 | "cpu": [ 54 | "arm" 55 | ], 56 | "dev": true, 57 | "license": "MIT", 58 | "optional": true, 59 | "os": [ 60 | "android" 61 | ], 62 | "engines": { 63 | "node": ">=18" 64 | } 65 | }, 66 | "node_modules/@esbuild/android-arm64": { 67 | "version": "0.25.2", 68 | "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.2.tgz", 69 | "integrity": "sha512-5ZAX5xOmTligeBaeNEPnPaeEuah53Id2tX4c2CVP3JaROTH+j4fnfHCkr1PjXMd78hMst+TlkfKcW/DlTq0i4w==", 70 | "cpu": [ 71 | "arm64" 72 | ], 73 | "dev": true, 74 | "license": "MIT", 75 | "optional": true, 76 | "os": [ 77 | "android" 78 | ], 79 | "engines": { 80 | "node": ">=18" 81 | } 82 | }, 83 | "node_modules/@esbuild/android-x64": { 84 | "version": "0.25.2", 85 | "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.2.tgz", 86 | "integrity": "sha512-Ffcx+nnma8Sge4jzddPHCZVRvIfQ0kMsUsCMcJRHkGJ1cDmhe4SsrYIjLUKn1xpHZybmOqCWwB0zQvsjdEHtkg==", 87 | "cpu": [ 88 | "x64" 89 | ], 90 | "dev": true, 91 | "license": "MIT", 92 | "optional": true, 93 | "os": [ 94 | "android" 95 | ], 96 | "engines": { 97 | "node": ">=18" 98 | } 99 | }, 100 | "node_modules/@esbuild/darwin-arm64": { 101 | "version": "0.25.2", 102 | "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.2.tgz", 103 | "integrity": "sha512-MpM6LUVTXAzOvN4KbjzU/q5smzryuoNjlriAIx+06RpecwCkL9JpenNzpKd2YMzLJFOdPqBpuub6eVRP5IgiSA==", 104 | "cpu": [ 105 | "arm64" 106 | ], 107 | "dev": true, 108 | "license": "MIT", 109 | "optional": true, 110 | "os": [ 111 | "darwin" 112 | ], 113 | "engines": { 114 | "node": ">=18" 115 | } 116 | }, 117 | "node_modules/@esbuild/darwin-x64": { 118 | "version": "0.25.2", 119 | "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.2.tgz", 120 | "integrity": "sha512-5eRPrTX7wFyuWe8FqEFPG2cU0+butQQVNcT4sVipqjLYQjjh8a8+vUTfgBKM88ObB85ahsnTwF7PSIt6PG+QkA==", 121 | "cpu": [ 122 | "x64" 123 | ], 124 | "dev": true, 125 | "license": "MIT", 126 | "optional": true, 127 | "os": [ 128 | "darwin" 129 | ], 130 | "engines": { 131 | "node": ">=18" 132 | } 133 | }, 134 | "node_modules/@esbuild/freebsd-arm64": { 135 | "version": "0.25.2", 136 | "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.2.tgz", 137 | "integrity": "sha512-mLwm4vXKiQ2UTSX4+ImyiPdiHjiZhIaE9QvC7sw0tZ6HoNMjYAqQpGyui5VRIi5sGd+uWq940gdCbY3VLvsO1w==", 138 | "cpu": [ 139 | "arm64" 140 | ], 141 | "dev": true, 142 | "license": "MIT", 143 | "optional": true, 144 | "os": [ 145 | "freebsd" 146 | ], 147 | "engines": { 148 | "node": ">=18" 149 | } 150 | }, 151 | "node_modules/@esbuild/freebsd-x64": { 152 | "version": "0.25.2", 153 | "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.2.tgz", 154 | "integrity": "sha512-6qyyn6TjayJSwGpm8J9QYYGQcRgc90nmfdUb0O7pp1s4lTY+9D0H9O02v5JqGApUyiHOtkz6+1hZNvNtEhbwRQ==", 155 | "cpu": [ 156 | "x64" 157 | ], 158 | "dev": true, 159 | "license": "MIT", 160 | "optional": true, 161 | "os": [ 162 | "freebsd" 163 | ], 164 | "engines": { 165 | "node": ">=18" 166 | } 167 | }, 168 | "node_modules/@esbuild/linux-arm": { 169 | "version": "0.25.2", 170 | "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.2.tgz", 171 | "integrity": "sha512-UHBRgJcmjJv5oeQF8EpTRZs/1knq6loLxTsjc3nxO9eXAPDLcWW55flrMVc97qFPbmZP31ta1AZVUKQzKTzb0g==", 172 | "cpu": [ 173 | "arm" 174 | ], 175 | "dev": true, 176 | "license": "MIT", 177 | "optional": true, 178 | "os": [ 179 | "linux" 180 | ], 181 | "engines": { 182 | "node": ">=18" 183 | } 184 | }, 185 | "node_modules/@esbuild/linux-arm64": { 186 | "version": "0.25.2", 187 | "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.2.tgz", 188 | "integrity": "sha512-gq/sjLsOyMT19I8obBISvhoYiZIAaGF8JpeXu1u8yPv8BE5HlWYobmlsfijFIZ9hIVGYkbdFhEqC0NvM4kNO0g==", 189 | "cpu": [ 190 | "arm64" 191 | ], 192 | "dev": true, 193 | "license": "MIT", 194 | "optional": true, 195 | "os": [ 196 | "linux" 197 | ], 198 | "engines": { 199 | "node": ">=18" 200 | } 201 | }, 202 | "node_modules/@esbuild/linux-ia32": { 203 | "version": "0.25.2", 204 | "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.2.tgz", 205 | "integrity": "sha512-bBYCv9obgW2cBP+2ZWfjYTU+f5cxRoGGQ5SeDbYdFCAZpYWrfjjfYwvUpP8MlKbP0nwZ5gyOU/0aUzZ5HWPuvQ==", 206 | "cpu": [ 207 | "ia32" 208 | ], 209 | "dev": true, 210 | "license": "MIT", 211 | "optional": true, 212 | "os": [ 213 | "linux" 214 | ], 215 | "engines": { 216 | "node": ">=18" 217 | } 218 | }, 219 | "node_modules/@esbuild/linux-loong64": { 220 | "version": "0.25.2", 221 | "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.2.tgz", 222 | "integrity": "sha512-SHNGiKtvnU2dBlM5D8CXRFdd+6etgZ9dXfaPCeJtz+37PIUlixvlIhI23L5khKXs3DIzAn9V8v+qb1TRKrgT5w==", 223 | "cpu": [ 224 | "loong64" 225 | ], 226 | "dev": true, 227 | "license": "MIT", 228 | "optional": true, 229 | "os": [ 230 | "linux" 231 | ], 232 | "engines": { 233 | "node": ">=18" 234 | } 235 | }, 236 | "node_modules/@esbuild/linux-mips64el": { 237 | "version": "0.25.2", 238 | "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.2.tgz", 239 | "integrity": "sha512-hDDRlzE6rPeoj+5fsADqdUZl1OzqDYow4TB4Y/3PlKBD0ph1e6uPHzIQcv2Z65u2K0kpeByIyAjCmjn1hJgG0Q==", 240 | "cpu": [ 241 | "mips64el" 242 | ], 243 | "dev": true, 244 | "license": "MIT", 245 | "optional": true, 246 | "os": [ 247 | "linux" 248 | ], 249 | "engines": { 250 | "node": ">=18" 251 | } 252 | }, 253 | "node_modules/@esbuild/linux-ppc64": { 254 | "version": "0.25.2", 255 | "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.2.tgz", 256 | "integrity": "sha512-tsHu2RRSWzipmUi9UBDEzc0nLc4HtpZEI5Ba+Omms5456x5WaNuiG3u7xh5AO6sipnJ9r4cRWQB2tUjPyIkc6g==", 257 | "cpu": [ 258 | "ppc64" 259 | ], 260 | "dev": true, 261 | "license": "MIT", 262 | "optional": true, 263 | "os": [ 264 | "linux" 265 | ], 266 | "engines": { 267 | "node": ">=18" 268 | } 269 | }, 270 | "node_modules/@esbuild/linux-riscv64": { 271 | "version": "0.25.2", 272 | "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.2.tgz", 273 | "integrity": "sha512-k4LtpgV7NJQOml/10uPU0s4SAXGnowi5qBSjaLWMojNCUICNu7TshqHLAEbkBdAszL5TabfvQ48kK84hyFzjnw==", 274 | "cpu": [ 275 | "riscv64" 276 | ], 277 | "dev": true, 278 | "license": "MIT", 279 | "optional": true, 280 | "os": [ 281 | "linux" 282 | ], 283 | "engines": { 284 | "node": ">=18" 285 | } 286 | }, 287 | "node_modules/@esbuild/linux-s390x": { 288 | "version": "0.25.2", 289 | "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.2.tgz", 290 | "integrity": "sha512-GRa4IshOdvKY7M/rDpRR3gkiTNp34M0eLTaC1a08gNrh4u488aPhuZOCpkF6+2wl3zAN7L7XIpOFBhnaE3/Q8Q==", 291 | "cpu": [ 292 | "s390x" 293 | ], 294 | "dev": true, 295 | "license": "MIT", 296 | "optional": true, 297 | "os": [ 298 | "linux" 299 | ], 300 | "engines": { 301 | "node": ">=18" 302 | } 303 | }, 304 | "node_modules/@esbuild/linux-x64": { 305 | "version": "0.25.2", 306 | "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.2.tgz", 307 | "integrity": "sha512-QInHERlqpTTZ4FRB0fROQWXcYRD64lAoiegezDunLpalZMjcUcld3YzZmVJ2H/Cp0wJRZ8Xtjtj0cEHhYc/uUg==", 308 | "cpu": [ 309 | "x64" 310 | ], 311 | "dev": true, 312 | "license": "MIT", 313 | "optional": true, 314 | "os": [ 315 | "linux" 316 | ], 317 | "engines": { 318 | "node": ">=18" 319 | } 320 | }, 321 | "node_modules/@esbuild/netbsd-arm64": { 322 | "version": "0.25.2", 323 | "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.2.tgz", 324 | "integrity": "sha512-talAIBoY5M8vHc6EeI2WW9d/CkiO9MQJ0IOWX8hrLhxGbro/vBXJvaQXefW2cP0z0nQVTdQ/eNyGFV1GSKrxfw==", 325 | "cpu": [ 326 | "arm64" 327 | ], 328 | "dev": true, 329 | "license": "MIT", 330 | "optional": true, 331 | "os": [ 332 | "netbsd" 333 | ], 334 | "engines": { 335 | "node": ">=18" 336 | } 337 | }, 338 | "node_modules/@esbuild/netbsd-x64": { 339 | "version": "0.25.2", 340 | "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.2.tgz", 341 | "integrity": "sha512-voZT9Z+tpOxrvfKFyfDYPc4DO4rk06qamv1a/fkuzHpiVBMOhpjK+vBmWM8J1eiB3OLSMFYNaOaBNLXGChf5tg==", 342 | "cpu": [ 343 | "x64" 344 | ], 345 | "dev": true, 346 | "license": "MIT", 347 | "optional": true, 348 | "os": [ 349 | "netbsd" 350 | ], 351 | "engines": { 352 | "node": ">=18" 353 | } 354 | }, 355 | "node_modules/@esbuild/openbsd-arm64": { 356 | "version": "0.25.2", 357 | "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.2.tgz", 358 | "integrity": "sha512-dcXYOC6NXOqcykeDlwId9kB6OkPUxOEqU+rkrYVqJbK2hagWOMrsTGsMr8+rW02M+d5Op5NNlgMmjzecaRf7Tg==", 359 | "cpu": [ 360 | "arm64" 361 | ], 362 | "dev": true, 363 | "license": "MIT", 364 | "optional": true, 365 | "os": [ 366 | "openbsd" 367 | ], 368 | "engines": { 369 | "node": ">=18" 370 | } 371 | }, 372 | "node_modules/@esbuild/openbsd-x64": { 373 | "version": "0.25.2", 374 | "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.2.tgz", 375 | "integrity": "sha512-t/TkWwahkH0Tsgoq1Ju7QfgGhArkGLkF1uYz8nQS/PPFlXbP5YgRpqQR3ARRiC2iXoLTWFxc6DJMSK10dVXluw==", 376 | "cpu": [ 377 | "x64" 378 | ], 379 | "dev": true, 380 | "license": "MIT", 381 | "optional": true, 382 | "os": [ 383 | "openbsd" 384 | ], 385 | "engines": { 386 | "node": ">=18" 387 | } 388 | }, 389 | "node_modules/@esbuild/sunos-x64": { 390 | "version": "0.25.2", 391 | "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.2.tgz", 392 | "integrity": "sha512-cfZH1co2+imVdWCjd+D1gf9NjkchVhhdpgb1q5y6Hcv9TP6Zi9ZG/beI3ig8TvwT9lH9dlxLq5MQBBgwuj4xvA==", 393 | "cpu": [ 394 | "x64" 395 | ], 396 | "dev": true, 397 | "license": "MIT", 398 | "optional": true, 399 | "os": [ 400 | "sunos" 401 | ], 402 | "engines": { 403 | "node": ">=18" 404 | } 405 | }, 406 | "node_modules/@esbuild/win32-arm64": { 407 | "version": "0.25.2", 408 | "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.2.tgz", 409 | "integrity": "sha512-7Loyjh+D/Nx/sOTzV8vfbB3GJuHdOQyrOryFdZvPHLf42Tk9ivBU5Aedi7iyX+x6rbn2Mh68T4qq1SDqJBQO5Q==", 410 | "cpu": [ 411 | "arm64" 412 | ], 413 | "dev": true, 414 | "license": "MIT", 415 | "optional": true, 416 | "os": [ 417 | "win32" 418 | ], 419 | "engines": { 420 | "node": ">=18" 421 | } 422 | }, 423 | "node_modules/@esbuild/win32-ia32": { 424 | "version": "0.25.2", 425 | "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.2.tgz", 426 | "integrity": "sha512-WRJgsz9un0nqZJ4MfhabxaD9Ft8KioqU3JMinOTvobbX6MOSUigSBlogP8QB3uxpJDsFS6yN+3FDBdqE5lg9kg==", 427 | "cpu": [ 428 | "ia32" 429 | ], 430 | "dev": true, 431 | "license": "MIT", 432 | "optional": true, 433 | "os": [ 434 | "win32" 435 | ], 436 | "engines": { 437 | "node": ">=18" 438 | } 439 | }, 440 | "node_modules/@esbuild/win32-x64": { 441 | "version": "0.25.2", 442 | "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.2.tgz", 443 | "integrity": "sha512-kM3HKb16VIXZyIeVrM1ygYmZBKybX8N4p754bw390wGO3Tf2j4L2/WYL+4suWujpgf6GBYs3jv7TyUivdd05JA==", 444 | "cpu": [ 445 | "x64" 446 | ], 447 | "dev": true, 448 | "license": "MIT", 449 | "optional": true, 450 | "os": [ 451 | "win32" 452 | ], 453 | "engines": { 454 | "node": ">=18" 455 | } 456 | }, 457 | "node_modules/@octokit/endpoint": { 458 | "version": "10.1.4", 459 | "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.4.tgz", 460 | "integrity": "sha512-OlYOlZIsfEVZm5HCSR8aSg02T2lbUWOsCQoPKfTXJwDzcHQBrVBGdGXb89dv2Kw2ToZaRtudp8O3ZIYoaOjKlA==", 461 | "license": "MIT", 462 | "dependencies": { 463 | "@octokit/types": "^14.0.0", 464 | "universal-user-agent": "^7.0.2" 465 | }, 466 | "engines": { 467 | "node": ">= 18" 468 | } 469 | }, 470 | "node_modules/@octokit/openapi-types": { 471 | "version": "25.0.0", 472 | "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.0.0.tgz", 473 | "integrity": "sha512-FZvktFu7HfOIJf2BScLKIEYjDsw6RKc7rBJCdvCTfKsVnx2GEB/Nbzjr29DUdb7vQhlzS/j8qDzdditP0OC6aw==", 474 | "license": "MIT" 475 | }, 476 | "node_modules/@octokit/request": { 477 | "version": "9.2.3", 478 | "resolved": "https://registry.npmjs.org/@octokit/request/-/request-9.2.3.tgz", 479 | "integrity": "sha512-Ma+pZU8PXLOEYzsWf0cn/gY+ME57Wq8f49WTXA8FMHp2Ps9djKw//xYJ1je8Hm0pR2lU9FUGeJRWOtxq6olt4w==", 480 | "license": "MIT", 481 | "dependencies": { 482 | "@octokit/endpoint": "^10.1.4", 483 | "@octokit/request-error": "^6.1.8", 484 | "@octokit/types": "^14.0.0", 485 | "fast-content-type-parse": "^2.0.0", 486 | "universal-user-agent": "^7.0.2" 487 | }, 488 | "engines": { 489 | "node": ">= 18" 490 | } 491 | }, 492 | "node_modules/@octokit/request-error": { 493 | "version": "6.1.8", 494 | "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-6.1.8.tgz", 495 | "integrity": "sha512-WEi/R0Jmq+IJKydWlKDmryPcmdYSVjL3ekaiEL1L9eo1sUnqMJ+grqmC9cjk7CA7+b2/T397tO5d8YLOH3qYpQ==", 496 | "license": "MIT", 497 | "dependencies": { 498 | "@octokit/types": "^14.0.0" 499 | }, 500 | "engines": { 501 | "node": ">= 18" 502 | } 503 | }, 504 | "node_modules/@octokit/types": { 505 | "version": "14.0.0", 506 | "resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.0.0.tgz", 507 | "integrity": "sha512-VVmZP0lEhbo2O1pdq63gZFiGCKkm8PPp8AUOijlwPO6hojEVjspA0MWKP7E4hbvGxzFKNqKr6p0IYtOH/Wf/zA==", 508 | "license": "MIT", 509 | "dependencies": { 510 | "@octokit/openapi-types": "^25.0.0" 511 | } 512 | }, 513 | "node_modules/@types/body-parser": { 514 | "version": "1.19.5", 515 | "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", 516 | "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", 517 | "dev": true, 518 | "license": "MIT", 519 | "dependencies": { 520 | "@types/connect": "*", 521 | "@types/node": "*" 522 | } 523 | }, 524 | "node_modules/@types/connect": { 525 | "version": "3.4.38", 526 | "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", 527 | "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", 528 | "dev": true, 529 | "license": "MIT", 530 | "dependencies": { 531 | "@types/node": "*" 532 | } 533 | }, 534 | "node_modules/@types/express": { 535 | "version": "4.17.21", 536 | "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", 537 | "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", 538 | "dev": true, 539 | "license": "MIT", 540 | "dependencies": { 541 | "@types/body-parser": "*", 542 | "@types/express-serve-static-core": "^4.17.33", 543 | "@types/qs": "*", 544 | "@types/serve-static": "*" 545 | } 546 | }, 547 | "node_modules/@types/express-serve-static-core": { 548 | "version": "4.19.5", 549 | "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.5.tgz", 550 | "integrity": "sha512-y6W03tvrACO72aijJ5uF02FRq5cgDR9lUxddQ8vyF+GvmjJQqbzDcJngEjURc+ZsG31VI3hODNZJ2URj86pzmg==", 551 | "dev": true, 552 | "license": "MIT", 553 | "dependencies": { 554 | "@types/node": "*", 555 | "@types/qs": "*", 556 | "@types/range-parser": "*", 557 | "@types/send": "*" 558 | } 559 | }, 560 | "node_modules/@types/http-errors": { 561 | "version": "2.0.4", 562 | "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", 563 | "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", 564 | "dev": true, 565 | "license": "MIT" 566 | }, 567 | "node_modules/@types/mime": { 568 | "version": "1.3.5", 569 | "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", 570 | "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", 571 | "dev": true, 572 | "license": "MIT" 573 | }, 574 | "node_modules/@types/node": { 575 | "version": "22.1.0", 576 | "resolved": "https://registry.npmjs.org/@types/node/-/node-22.1.0.tgz", 577 | "integrity": "sha512-AOmuRF0R2/5j1knA3c6G3HOk523Ga+l+ZXltX8SF1+5oqcXijjfTd8fY3XRZqSihEu9XhtQnKYLmkFaoxgsJHw==", 578 | "license": "MIT", 579 | "dependencies": { 580 | "undici-types": "~6.13.0" 581 | } 582 | }, 583 | "node_modules/@types/node-fetch": { 584 | "version": "2.6.11", 585 | "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.11.tgz", 586 | "integrity": "sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==", 587 | "license": "MIT", 588 | "dependencies": { 589 | "@types/node": "*", 590 | "form-data": "^4.0.0" 591 | } 592 | }, 593 | "node_modules/@types/qs": { 594 | "version": "6.9.15", 595 | "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.15.tgz", 596 | "integrity": "sha512-uXHQKES6DQKKCLh441Xv/dwxOq1TVS3JPUMlEqoEglvlhR6Mxnlew/Xq/LRVHpLyk7iK3zODe1qYHIMltO7XGg==", 597 | "dev": true, 598 | "license": "MIT" 599 | }, 600 | "node_modules/@types/range-parser": { 601 | "version": "1.2.7", 602 | "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", 603 | "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", 604 | "dev": true, 605 | "license": "MIT" 606 | }, 607 | "node_modules/@types/send": { 608 | "version": "0.17.4", 609 | "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", 610 | "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", 611 | "dev": true, 612 | "license": "MIT", 613 | "dependencies": { 614 | "@types/mime": "^1", 615 | "@types/node": "*" 616 | } 617 | }, 618 | "node_modules/@types/serve-static": { 619 | "version": "1.15.7", 620 | "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", 621 | "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", 622 | "dev": true, 623 | "license": "MIT", 624 | "dependencies": { 625 | "@types/http-errors": "*", 626 | "@types/node": "*", 627 | "@types/send": "*" 628 | } 629 | }, 630 | "node_modules/abort-controller": { 631 | "version": "3.0.0", 632 | "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", 633 | "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", 634 | "license": "MIT", 635 | "dependencies": { 636 | "event-target-shim": "^5.0.0" 637 | }, 638 | "engines": { 639 | "node": ">=6.5" 640 | } 641 | }, 642 | "node_modules/agentkeepalive": { 643 | "version": "4.5.0", 644 | "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.5.0.tgz", 645 | "integrity": "sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==", 646 | "license": "MIT", 647 | "dependencies": { 648 | "humanize-ms": "^1.2.1" 649 | }, 650 | "engines": { 651 | "node": ">= 8.0.0" 652 | } 653 | }, 654 | "node_modules/asynckit": { 655 | "version": "0.4.0", 656 | "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", 657 | "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", 658 | "license": "MIT" 659 | }, 660 | "node_modules/combined-stream": { 661 | "version": "1.0.8", 662 | "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", 663 | "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", 664 | "license": "MIT", 665 | "dependencies": { 666 | "delayed-stream": "~1.0.0" 667 | }, 668 | "engines": { 669 | "node": ">= 0.8" 670 | } 671 | }, 672 | "node_modules/delayed-stream": { 673 | "version": "1.0.0", 674 | "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", 675 | "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", 676 | "license": "MIT", 677 | "engines": { 678 | "node": ">=0.4.0" 679 | } 680 | }, 681 | "node_modules/esbuild": { 682 | "version": "0.25.2", 683 | "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.2.tgz", 684 | "integrity": "sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ==", 685 | "dev": true, 686 | "hasInstallScript": true, 687 | "license": "MIT", 688 | "bin": { 689 | "esbuild": "bin/esbuild" 690 | }, 691 | "engines": { 692 | "node": ">=18" 693 | }, 694 | "optionalDependencies": { 695 | "@esbuild/aix-ppc64": "0.25.2", 696 | "@esbuild/android-arm": "0.25.2", 697 | "@esbuild/android-arm64": "0.25.2", 698 | "@esbuild/android-x64": "0.25.2", 699 | "@esbuild/darwin-arm64": "0.25.2", 700 | "@esbuild/darwin-x64": "0.25.2", 701 | "@esbuild/freebsd-arm64": "0.25.2", 702 | "@esbuild/freebsd-x64": "0.25.2", 703 | "@esbuild/linux-arm": "0.25.2", 704 | "@esbuild/linux-arm64": "0.25.2", 705 | "@esbuild/linux-ia32": "0.25.2", 706 | "@esbuild/linux-loong64": "0.25.2", 707 | "@esbuild/linux-mips64el": "0.25.2", 708 | "@esbuild/linux-ppc64": "0.25.2", 709 | "@esbuild/linux-riscv64": "0.25.2", 710 | "@esbuild/linux-s390x": "0.25.2", 711 | "@esbuild/linux-x64": "0.25.2", 712 | "@esbuild/netbsd-arm64": "0.25.2", 713 | "@esbuild/netbsd-x64": "0.25.2", 714 | "@esbuild/openbsd-arm64": "0.25.2", 715 | "@esbuild/openbsd-x64": "0.25.2", 716 | "@esbuild/sunos-x64": "0.25.2", 717 | "@esbuild/win32-arm64": "0.25.2", 718 | "@esbuild/win32-ia32": "0.25.2", 719 | "@esbuild/win32-x64": "0.25.2" 720 | } 721 | }, 722 | "node_modules/event-target-shim": { 723 | "version": "5.0.1", 724 | "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", 725 | "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", 726 | "license": "MIT", 727 | "engines": { 728 | "node": ">=6" 729 | } 730 | }, 731 | "node_modules/fast-content-type-parse": { 732 | "version": "2.0.1", 733 | "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-2.0.1.tgz", 734 | "integrity": "sha512-nGqtvLrj5w0naR6tDPfB4cUmYCqouzyQiz6C5y/LtcDllJdrcc6WaWW6iXyIIOErTa/XRybj28aasdn4LkVk6Q==", 735 | "funding": [ 736 | { 737 | "type": "github", 738 | "url": "https://github.com/sponsors/fastify" 739 | }, 740 | { 741 | "type": "opencollective", 742 | "url": "https://opencollective.com/fastify" 743 | } 744 | ], 745 | "license": "MIT" 746 | }, 747 | "node_modules/form-data": { 748 | "version": "4.0.0", 749 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", 750 | "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", 751 | "license": "MIT", 752 | "dependencies": { 753 | "asynckit": "^0.4.0", 754 | "combined-stream": "^1.0.8", 755 | "mime-types": "^2.1.12" 756 | }, 757 | "engines": { 758 | "node": ">= 6" 759 | } 760 | }, 761 | "node_modules/form-data-encoder": { 762 | "version": "1.7.2", 763 | "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz", 764 | "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==", 765 | "license": "MIT" 766 | }, 767 | "node_modules/formdata-node": { 768 | "version": "4.4.1", 769 | "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz", 770 | "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==", 771 | "license": "MIT", 772 | "dependencies": { 773 | "node-domexception": "1.0.0", 774 | "web-streams-polyfill": "4.0.0-beta.3" 775 | }, 776 | "engines": { 777 | "node": ">= 12.20" 778 | } 779 | }, 780 | "node_modules/fsevents": { 781 | "version": "2.3.3", 782 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", 783 | "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", 784 | "dev": true, 785 | "hasInstallScript": true, 786 | "license": "MIT", 787 | "optional": true, 788 | "os": [ 789 | "darwin" 790 | ], 791 | "engines": { 792 | "node": "^8.16.0 || ^10.6.0 || >=11.0.0" 793 | } 794 | }, 795 | "node_modules/get-tsconfig": { 796 | "version": "4.7.6", 797 | "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.7.6.tgz", 798 | "integrity": "sha512-ZAqrLlu18NbDdRaHq+AKXzAmqIUPswPWKUchfytdAjiRFnCe5ojG2bstg6mRiZabkKfCoL/e98pbBELIV/YCeA==", 799 | "dev": true, 800 | "license": "MIT", 801 | "dependencies": { 802 | "resolve-pkg-maps": "^1.0.0" 803 | }, 804 | "funding": { 805 | "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" 806 | } 807 | }, 808 | "node_modules/humanize-ms": { 809 | "version": "1.2.1", 810 | "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", 811 | "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", 812 | "license": "MIT", 813 | "dependencies": { 814 | "ms": "^2.0.0" 815 | } 816 | }, 817 | "node_modules/mime-db": { 818 | "version": "1.52.0", 819 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", 820 | "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", 821 | "license": "MIT", 822 | "engines": { 823 | "node": ">= 0.6" 824 | } 825 | }, 826 | "node_modules/mime-types": { 827 | "version": "2.1.35", 828 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", 829 | "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", 830 | "license": "MIT", 831 | "dependencies": { 832 | "mime-db": "1.52.0" 833 | }, 834 | "engines": { 835 | "node": ">= 0.6" 836 | } 837 | }, 838 | "node_modules/ms": { 839 | "version": "2.0.0", 840 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 841 | "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", 842 | "license": "MIT" 843 | }, 844 | "node_modules/node-domexception": { 845 | "version": "1.0.0", 846 | "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", 847 | "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", 848 | "funding": [ 849 | { 850 | "type": "github", 851 | "url": "https://github.com/sponsors/jimmywarting" 852 | }, 853 | { 854 | "type": "github", 855 | "url": "https://paypal.me/jimmywarting" 856 | } 857 | ], 858 | "license": "MIT", 859 | "engines": { 860 | "node": ">=10.5.0" 861 | } 862 | }, 863 | "node_modules/node-fetch": { 864 | "version": "2.7.0", 865 | "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", 866 | "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", 867 | "license": "MIT", 868 | "dependencies": { 869 | "whatwg-url": "^5.0.0" 870 | }, 871 | "engines": { 872 | "node": "4.x || >=6.0.0" 873 | }, 874 | "peerDependencies": { 875 | "encoding": "^0.1.0" 876 | }, 877 | "peerDependenciesMeta": { 878 | "encoding": { 879 | "optional": true 880 | } 881 | } 882 | }, 883 | "node_modules/openai": { 884 | "version": "4.55.0", 885 | "resolved": "https://registry.npmjs.org/openai/-/openai-4.55.0.tgz", 886 | "integrity": "sha512-BR3TUybzdqNeBMgEFvxgBrFks9FY2NoP2jyTf7LT4UxPv8chevRKSxKezsINVSeQ/QLA12CALR1oco6KVdVpVA==", 887 | "license": "Apache-2.0", 888 | "dependencies": { 889 | "@types/node": "^18.11.18", 890 | "@types/node-fetch": "^2.6.4", 891 | "abort-controller": "^3.0.0", 892 | "agentkeepalive": "^4.2.1", 893 | "form-data-encoder": "1.7.2", 894 | "formdata-node": "^4.3.2", 895 | "node-fetch": "^2.6.7" 896 | }, 897 | "bin": { 898 | "openai": "bin/cli" 899 | }, 900 | "peerDependencies": { 901 | "zod": "^3.23.8" 902 | }, 903 | "peerDependenciesMeta": { 904 | "zod": { 905 | "optional": true 906 | } 907 | } 908 | }, 909 | "node_modules/openai/node_modules/@types/node": { 910 | "version": "18.19.43", 911 | "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.43.tgz", 912 | "integrity": "sha512-Mw/YlgXnyJdEwLoFv2dpuJaDFriX+Pc+0qOBJ57jC1H6cDxIj2xc5yUrdtArDVG0m+KV6622a4p2tenEqB3C/g==", 913 | "license": "MIT", 914 | "dependencies": { 915 | "undici-types": "~5.26.4" 916 | } 917 | }, 918 | "node_modules/openai/node_modules/undici-types": { 919 | "version": "5.26.5", 920 | "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", 921 | "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", 922 | "license": "MIT" 923 | }, 924 | "node_modules/resolve-pkg-maps": { 925 | "version": "1.0.0", 926 | "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", 927 | "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", 928 | "dev": true, 929 | "license": "MIT", 930 | "funding": { 931 | "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" 932 | } 933 | }, 934 | "node_modules/tr46": { 935 | "version": "0.0.3", 936 | "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", 937 | "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", 938 | "license": "MIT" 939 | }, 940 | "node_modules/tsx": { 941 | "version": "4.19.3", 942 | "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.3.tgz", 943 | "integrity": "sha512-4H8vUNGNjQ4V2EOoGw005+c+dGuPSnhpPBPHBtsZdGZBk/iJb4kguGlPWaZTZ3q5nMtFOEsY0nRDlh9PJyd6SQ==", 944 | "dev": true, 945 | "license": "MIT", 946 | "dependencies": { 947 | "esbuild": "~0.25.0", 948 | "get-tsconfig": "^4.7.5" 949 | }, 950 | "bin": { 951 | "tsx": "dist/cli.mjs" 952 | }, 953 | "engines": { 954 | "node": ">=18.0.0" 955 | }, 956 | "optionalDependencies": { 957 | "fsevents": "~2.3.3" 958 | } 959 | }, 960 | "node_modules/typescript": { 961 | "version": "5.5.4", 962 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", 963 | "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", 964 | "dev": true, 965 | "license": "Apache-2.0", 966 | "bin": { 967 | "tsc": "bin/tsc", 968 | "tsserver": "bin/tsserver" 969 | }, 970 | "engines": { 971 | "node": ">=14.17" 972 | } 973 | }, 974 | "node_modules/undici-types": { 975 | "version": "6.13.0", 976 | "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.13.0.tgz", 977 | "integrity": "sha512-xtFJHudx8S2DSoujjMd1WeWvn7KKWFRESZTMeL1RptAYERu29D6jphMjjY+vn96jvN3kVPDNxU/E13VTaXj6jg==", 978 | "license": "MIT" 979 | }, 980 | "node_modules/universal-user-agent": { 981 | "version": "7.0.2", 982 | "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.2.tgz", 983 | "integrity": "sha512-0JCqzSKnStlRRQfCdowvqy3cy0Dvtlb8xecj/H8JFZuCze4rwjPZQOgvFvn0Ws/usCHQFGpyr+pB9adaGwXn4Q==", 984 | "license": "ISC" 985 | }, 986 | "node_modules/web-streams-polyfill": { 987 | "version": "4.0.0-beta.3", 988 | "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz", 989 | "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==", 990 | "license": "MIT", 991 | "engines": { 992 | "node": ">= 14" 993 | } 994 | }, 995 | "node_modules/webidl-conversions": { 996 | "version": "3.0.1", 997 | "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", 998 | "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", 999 | "license": "BSD-2-Clause" 1000 | }, 1001 | "node_modules/whatwg-url": { 1002 | "version": "5.0.0", 1003 | "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", 1004 | "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", 1005 | "license": "MIT", 1006 | "dependencies": { 1007 | "tr46": "~0.0.3", 1008 | "webidl-conversions": "^3.0.0" 1009 | } 1010 | } 1011 | } 1012 | } 1013 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gh-models", 3 | "version": "1.0.0", 4 | "type": "module", 5 | "scripts": { 6 | "start": "node dist/index.js", 7 | "build": "tsc", 8 | "dev": "tsx --watch src/index.ts" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "description": "", 14 | "dependencies": { 15 | "@copilot-extensions/preview-sdk": "^5.0.1", 16 | "openai": "^4.55.0" 17 | }, 18 | "devDependencies": { 19 | "@types/express": "^4.17.21", 20 | "@types/node": "^22.1.0", 21 | "tsx": "^4.19.3", 22 | "typescript": "^5.5.4" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/functions.ts: -------------------------------------------------------------------------------- 1 | import OpenAI from "openai"; 2 | import { ModelsAPI } from "./models-api.js"; 3 | 4 | // defaultModel is the model used for internal calls - for tool calling, 5 | // or just for chat completions. 6 | export const defaultModel = "gpt-4o-mini"; 7 | 8 | // RunnerResponse is the response from a function call. 9 | export interface RunnerResponse { 10 | model: string; 11 | messages: OpenAI.ChatCompletionMessageParam[]; 12 | } 13 | 14 | export abstract class Tool { 15 | modelsAPI: ModelsAPI; 16 | static definition: OpenAI.FunctionDefinition; 17 | 18 | constructor(modelsAPI: ModelsAPI) { 19 | this.modelsAPI = modelsAPI; 20 | } 21 | 22 | static get tool(): OpenAI.Chat.Completions.ChatCompletionTool { 23 | return { 24 | type: "function", 25 | function: this.definition, 26 | }; 27 | } 28 | 29 | abstract execute( 30 | messages: OpenAI.ChatCompletionMessageParam[], 31 | args: object 32 | ): Promise; 33 | } 34 | -------------------------------------------------------------------------------- /src/functions/describe-model.ts: -------------------------------------------------------------------------------- 1 | import OpenAI from "openai"; 2 | import { RunnerResponse, defaultModel, Tool } from "../functions.js"; 3 | 4 | export class describeModel extends Tool { 5 | static definition = { 6 | name: "describe_model", 7 | description: "Describes a specific model.", 8 | parameters: { 9 | type: "object", 10 | properties: { 11 | model: { 12 | type: "string", 13 | description: [ 14 | 'The model to describe. Looks like "model-name". For example, `Phi-3-medium-128k-instruct` or `gpt-4o`.', 15 | 'The list of models is available in the context window of the chat, in the `<-- LIST OF MODELS -->` section.', 16 | 'If the model name is not found in the list of models, pick the closest matching model from the list.', 17 | ].join("\n"), 18 | }, 19 | }, 20 | required: ["model"], 21 | }, 22 | }; 23 | 24 | async execute( 25 | messages: OpenAI.ChatCompletionMessageParam[], 26 | args: { model: string } 27 | ): Promise { 28 | const [model, modelSchema] = await Promise.all([ 29 | this.modelsAPI.getModel(args.model), 30 | this.modelsAPI.getModelSchema(args.model), 31 | ]); 32 | 33 | const systemMessage = [ 34 | "The user is asking about the AI model with the following details:", 35 | `\tModel Name: ${model.name}`, 36 | `\tModel Version: ${model.version}`, 37 | `\tPublisher: ${model.publisher}`, 38 | `\tModel Registry: ${model.registryName}`, 39 | `\tLicense: ${model.license}`, 40 | `\tTask: ${model.inferenceTasks.join(", ")}`, 41 | `\tDescription: ${model.description}`, 42 | `\tSummary: ${model.summary}`, 43 | "\n", 44 | "API requests for this model use the following schema:", 45 | "\n", 46 | "```json", 47 | JSON.stringify(modelSchema, null, 2), 48 | "```", 49 | ].join("\n"); 50 | 51 | return { 52 | model: defaultModel, 53 | messages: [{ role: "system", content: systemMessage }, ...messages], 54 | }; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/functions/execute-model.ts: -------------------------------------------------------------------------------- 1 | import OpenAI from "openai"; 2 | import { RunnerResponse, Tool } from "../functions.js"; 3 | 4 | type MessageWithReferences = OpenAI.ChatCompletionMessageParam & { 5 | copilot_references: Reference[]; 6 | }; 7 | 8 | interface Reference { 9 | type: string; 10 | data: any; 11 | id: string; 12 | metadata: any; 13 | } 14 | 15 | export class executeModel extends Tool { 16 | static definition = { 17 | name: "execute_model", 18 | description: `This function sends the prompt from the user's message to the specified model.". 19 | Example Queries (IMPORTANT: Phrasing doesn't have to match): 20 | - using gpt-4o-mini: what is 1+1? 21 | - using Mistral-large: what is the capital of France? 22 | - using cohere-command-r-plus: explain this code. 23 | `, 24 | parameters: { 25 | type: "object", 26 | properties: { 27 | model: { 28 | type: "string", 29 | description: [ 30 | "The name of the model to execute. It is ONLY the name of the model, not the publisher or registry.", 31 | "For example: `gpt-4o`, or `cohere-command-r-plus`.", 32 | "The list of models is available in the context window of the chat, in the `<-- LIST OF MODELS -->` section.", 33 | "If the model name is not found in the list of models, pick the closest matching model from the list.", 34 | ].join("\n"), 35 | }, 36 | instruction: { 37 | type: "string", 38 | description: "The instruction to execute.", 39 | }, 40 | }, 41 | required: ["model", "instruction"], 42 | }, 43 | }; 44 | 45 | async execute( 46 | messages: MessageWithReferences[], 47 | args: { 48 | model: string; 49 | instruction: string; 50 | } 51 | ): Promise { 52 | // Check if the user included any code references in their last message 53 | const lastMessage = messages[messages.length - 1]; 54 | let importantRefs: Reference[] = []; 55 | if (lastMessage.copilot_references) { 56 | importantRefs = lastMessage.copilot_references.filter((ref) => ref.type === "client.selection" || ref.type === "client.file"); 57 | } 58 | 59 | const content = [ 60 | `The user has chosen to use the model named ${args.model}.`, 61 | ]; 62 | 63 | if (importantRefs.length > 0) { 64 | content.push( 65 | "The user included the following context - you may find information in this context useful for your response:", 66 | JSON.stringify(importantRefs) 67 | ); 68 | } 69 | 70 | return { 71 | model: args.model, 72 | messages: [ 73 | { 74 | role: ["o1-mini", "o1-preview"].includes(args.model) ? "assistant" : "system", 75 | content: content.join("\n"), 76 | }, 77 | { role: "user", content: args.instruction }, 78 | ], 79 | }; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/functions/list-models.ts: -------------------------------------------------------------------------------- 1 | import OpenAI from "openai"; 2 | import { RunnerResponse, defaultModel, Tool } from "../functions.js"; 3 | 4 | export class listModels extends Tool { 5 | static definition = { 6 | name: "list_models", 7 | description: 8 | "This function lists the AI models available in GitHub Models.", 9 | parameters: { 10 | type: "object", 11 | properties: {}, 12 | description: 13 | "This function does not require any input parameters. It simply returns a list of models.", 14 | }, 15 | }; 16 | 17 | async execute( 18 | messages: OpenAI.ChatCompletionMessageParam[] 19 | ): Promise { 20 | const models = await this.modelsAPI.listModels(); 21 | 22 | const systemMessage = [ 23 | "The user is asking for a list of available models.", 24 | "Respond with a concise and readable list of the models, with a short description for each one.", 25 | "Use markdown formatting to make each description more readable.", 26 | "Begin each model's description with a header consisting of the model's name", 27 | "That list of models is as follows:", 28 | JSON.stringify( 29 | models.map((model) => ({ 30 | name: model.displayName, 31 | publisher: model.publisher, 32 | description: model.summary, 33 | })) 34 | ), 35 | ]; 36 | 37 | return { 38 | model: defaultModel, 39 | messages: [ 40 | { role: "system", content: systemMessage.join("\n") }, 41 | ...messages, 42 | ], 43 | }; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/functions/recommend-model.ts: -------------------------------------------------------------------------------- 1 | import OpenAI from "openai"; 2 | import { RunnerResponse, defaultModel, Tool } from "../functions.js"; 3 | 4 | export class recommendModel extends Tool { 5 | static definition = { 6 | name: "recommend_model", 7 | description: 8 | "Determines and recommends the most appropriate machine learning model based on the provided use-case. This function uses the available list of models to make the recommendation.", 9 | parameters: { 10 | type: "object", 11 | properties: {}, 12 | description: 13 | "This function does not require any input parameters. It uses internal logic to recommend the best model based on the provided use-case.", 14 | }, 15 | }; 16 | 17 | async execute( 18 | messages: OpenAI.ChatCompletionMessageParam[] 19 | ): Promise { 20 | const models = await this.modelsAPI.listModels(); 21 | 22 | const systemMessage = [ 23 | "The user is asking for you to recommend the right model for their use-case.", 24 | "Explain your reasoning, and why you recommend the model you choose.", 25 | "Provide a summary of the model's capabilities and limitations.", 26 | "Include any relevant information that the user should know.", 27 | "Use the available models to make your recommendation.", 28 | "The list of available models is as follows:", 29 | ]; 30 | 31 | for (const model of models) { 32 | systemMessage.push( 33 | [ 34 | `\t- Model Name: ${model.name}`, 35 | `\t\tModel Version: ${model.version}`, 36 | `\t\tPublisher: ${model.publisher}`, 37 | `\t\tModel Registry: ${model.registryName}`, 38 | `\t\tLicense: ${model.license}`, 39 | `\t\tTask: ${model.inferenceTasks.join(", ")}`, 40 | `\t\tSummary: ${model.summary}`, 41 | ].join("\n") 42 | ); 43 | } 44 | 45 | return { 46 | model: defaultModel, 47 | messages: [ 48 | { role: "system", content: systemMessage.join("\n") }, 49 | ...messages, 50 | ], 51 | }; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { createServer, type IncomingMessage } from "node:http"; 2 | 3 | import { verifyAndParseRequest, transformPayloadForOpenAICompatibility, getFunctionCalls, createDoneEvent, createReferencesEvent } from "@copilot-extensions/preview-sdk"; 4 | import OpenAI from "openai"; 5 | 6 | import { describeModel } from "./functions/describe-model.js"; 7 | import { executeModel } from "./functions/execute-model.js"; 8 | import { listModels } from "./functions/list-models.js"; 9 | import { RunnerResponse } from "./functions.js"; 10 | import { recommendModel } from "./functions/recommend-model.js"; 11 | import { ModelsAPI } from "./models-api.js"; 12 | 13 | const server = createServer(async (request, response) => { 14 | if (request.method === "GET") { 15 | response.statusCode = 200; 16 | response.end(`OK`); 17 | return; 18 | } 19 | 20 | const body = await getBody(request); 21 | 22 | let verifyAndParseRequestResult: Awaited>; 23 | const apiKey = request.headers["x-github-token"] as string; 24 | try { 25 | const signature = request.headers["x-github-public-key-signature"] as string; 26 | const keyID = request.headers["x-github-public-key-identifier"] as string; 27 | verifyAndParseRequestResult = await verifyAndParseRequest(body, signature, keyID, { 28 | token: apiKey, 29 | }); 30 | } catch (err) { 31 | console.error(err); 32 | response.statusCode = 401 33 | response.end("Unauthorized"); 34 | return 35 | } 36 | 37 | const { isValidRequest, payload } = verifyAndParseRequestResult 38 | 39 | if (!isValidRequest) { 40 | console.log("Signature verification failed"); 41 | response.statusCode = 401 42 | response.end("Unauthorized"); 43 | } 44 | 45 | console.log("Signature verified"); 46 | 47 | const compatibilityPayload = transformPayloadForOpenAICompatibility(payload); 48 | 49 | // Use the GitHub API token sent in the request 50 | if (!apiKey) { 51 | response.statusCode = 400 52 | response.end() 53 | return; 54 | } 55 | 56 | // List of functions that are available to be called 57 | const modelsAPI = new ModelsAPI(apiKey); 58 | 59 | const functions = [listModels, describeModel, executeModel, recommendModel]; 60 | 61 | // Use the Copilot API to determine which function to execute 62 | const capiClient = new OpenAI({ 63 | baseURL: "https://api.githubcopilot.com", 64 | apiKey, 65 | }); 66 | 67 | // Prepend a system message that includes the list of models, so that 68 | // tool calls can better select the right model to use. 69 | const models = await modelsAPI.listModels(); 70 | 71 | const toolCallMessages = [ 72 | { 73 | role: "system" as const, 74 | content: [ 75 | "You are an extension of GitHub Copilot, built to interact with GitHub Models.", 76 | "GitHub Models is a language model playground, where you can experiment with different models and see how they respond to your prompts.", 77 | "Here is a list of some of the models available to the user:", 78 | "<-- LIST OF MODELS -->", 79 | JSON.stringify( 80 | [...models.map((model) => ({ 81 | friendly_name: model.displayName, 82 | name: model.name, 83 | publisher: model.publisher, 84 | registry: model.registryName, 85 | description: model.summary, 86 | })), 87 | { 88 | friendly_name: "OpenAI o1-mini", 89 | name: "o1-mini", 90 | publisher: "openai", 91 | model_registry: "azure-openai", 92 | description: "Smaller, faster, and 80% cheaper than o1-preview, performs well at code generation and small context operations." 93 | }, 94 | { 95 | friendly_name: "OpenAI o1-preview", 96 | name: "o1-preview", 97 | publisher: "openai", 98 | model_registry: "azure-openai", 99 | description: "Focused on advanced reasoning and solving complex problems, including math and science tasks. Ideal for applications that require deep contextual understanding and agentic workflows." 100 | }, 101 | ] 102 | ), 103 | "<-- END OF LIST OF MODELS -->", 104 | ].join("\n"), 105 | }, 106 | ...compatibilityPayload.messages, 107 | ]; 108 | 109 | console.time("tool-call"); 110 | const toolCaller = await capiClient.chat.completions.create({ 111 | stream: false, 112 | model: "gpt-4o", 113 | messages: toolCallMessages, 114 | tools: functions.map((f) => f.tool), 115 | }); 116 | console.timeEnd("tool-call"); 117 | 118 | const [functionToCall] = getFunctionCalls( 119 | // @ts-expect-error - type error due to Copilot/OpenAI SDKs interop, I'll look into it ~@gr2m 120 | toolCaller.choices[0] 121 | ) 122 | 123 | if ( 124 | !functionToCall 125 | ) { 126 | console.log("No tool call found"); 127 | // No tool to call, so just call the model with the original messages 128 | const stream = await capiClient.chat.completions.create({ 129 | stream: true, 130 | model: "gpt-4o", 131 | messages: compatibilityPayload.messages, 132 | }) 133 | 134 | for await (const chunk of stream) { 135 | const chunkStr = "data: " + JSON.stringify(chunk) + "\n\n"; 136 | response.write(chunkStr); 137 | } 138 | 139 | response.end(createDoneEvent()); 140 | return; 141 | } 142 | 143 | const args = JSON.parse(functionToCall.function.arguments); 144 | 145 | console.time("function-exec"); 146 | let functionCallRes: RunnerResponse; 147 | try { 148 | console.log("Executing function", functionToCall.function.name); 149 | const funcClass = functions.find( 150 | (f) => f.definition.name === functionToCall.function.name 151 | ); 152 | if (!funcClass) { 153 | throw new Error("Unknown function"); 154 | } 155 | 156 | console.log("\t with args", args); 157 | const func = new funcClass(modelsAPI); 158 | functionCallRes = await func.execute( 159 | compatibilityPayload.messages, 160 | args 161 | ); 162 | } catch (err) { 163 | console.error(err); 164 | response.statusCode = 500 165 | response.end(); 166 | return; 167 | } 168 | console.timeEnd("function-exec"); 169 | 170 | // Now that we have a tool result, let's use it to call the model. 171 | try { 172 | let stream: AsyncIterable; 173 | 174 | if (functionToCall.function.name === executeModel.definition.name) { 175 | // First, let's write a reference with the model we're executing. 176 | // Fetch the model data from the index (already in-memory) so we have all the information we need 177 | // to build out the reference URLs 178 | const modelData = await modelsAPI.getModelFromIndex(functionCallRes.model); 179 | const sseData = { 180 | type: "models.reference", 181 | id: `models.reference.${modelData.name}`, 182 | data: { 183 | model: functionCallRes.model 184 | }, 185 | is_implicit: false, 186 | metadata: { 187 | display_name: `Model: ${modelData.name}`, 188 | display_icon: "icon", 189 | display_url: `https://github.com/marketplace/models/${modelData.registryName}/${modelData.name}`, 190 | } 191 | }; 192 | const event = createReferencesEvent([sseData]); 193 | response.write(event); 194 | 195 | if (["o1-mini", "o1-preview"].includes(args.model)) { 196 | // for non-streaming models, we need to still stream the response back, so we build the stream ourselves 197 | stream = (async function*() { 198 | const result = await modelsAPI.inference.chat.completions.create({ 199 | model: functionCallRes.model, 200 | messages: functionCallRes.messages 201 | }); 202 | yield result; 203 | })(); 204 | } else { 205 | stream = await modelsAPI.inference.chat.completions.create({ 206 | model: functionCallRes.model, 207 | messages: functionCallRes.messages, 208 | stream: true 209 | }); 210 | } 211 | } else { 212 | stream = await capiClient.chat.completions.create({ 213 | stream: true, 214 | model: "gpt-4o", 215 | messages: functionCallRes.messages, 216 | }); 217 | } 218 | 219 | console.time("streaming"); 220 | for await (const chunk of stream) { 221 | const chunkStr = "data: " + JSON.stringify(chunk) + "\n\n"; 222 | response.write(chunkStr); 223 | } 224 | 225 | response.end(createDoneEvent()); 226 | console.timeEnd("streaming"); 227 | } catch (err) { 228 | console.error(err); 229 | 230 | if ((err as any).response && (err as any).response.status === 400) { 231 | console.error('Error 400:', (err as any).response.data); 232 | } 233 | 234 | response.statusCode = 500 235 | response.write("data: Something went wrong\n\n") 236 | response.end() 237 | } 238 | }); 239 | 240 | const port = process.env.PORT || "3000" 241 | server.listen(port); 242 | console.log(`Server running at http://localhost:${port}`); 243 | 244 | function getBody(request: IncomingMessage): Promise { 245 | return new Promise((resolve) => { 246 | const bodyParts: Buffer[] = []; 247 | let body; 248 | request 249 | .on("data", (chunk: Buffer) => { 250 | bodyParts.push(chunk); 251 | }) 252 | .on("end", () => { 253 | resolve(Buffer.concat(bodyParts).toString()); 254 | }); 255 | }); 256 | } 257 | -------------------------------------------------------------------------------- /src/models-api.ts: -------------------------------------------------------------------------------- 1 | import OpenAI from "openai"; 2 | 3 | // Model is the structure of a model in the model catalog. 4 | export interface Model { 5 | name: string; 6 | displayName: string; 7 | version: string; 8 | publisher: string; 9 | registryName: string; 10 | license: string; 11 | inferenceTasks: string[]; 12 | description?: string; 13 | summary: string; 14 | } 15 | 16 | export type ModelSchema = { 17 | parameters: ModelSchemaParameter[]; 18 | capabilities: Record; 19 | }; 20 | 21 | export type ModelSchemaParameter = { 22 | key: string; 23 | type: "number" | "integer" | "array" | "string" | "boolean"; 24 | payloadPath: string; 25 | default?: number | string | boolean | any[]; 26 | min?: number; 27 | max?: number; 28 | required: boolean; 29 | description?: string; 30 | friendlyName?: string; 31 | }; 32 | 33 | export class ModelsAPI { 34 | inference: OpenAI; 35 | private _models: Model[] | null = null; 36 | 37 | constructor(apiKey: string) { 38 | this.inference = new OpenAI({ 39 | baseURL: "https://models.inference.ai.azure.com", 40 | apiKey, 41 | }); 42 | } 43 | 44 | async getModel(modelName: string): Promise { 45 | const modelFromIndex = await this.getModelFromIndex(modelName); 46 | 47 | const modelRes = await fetch( 48 | `https://api.catalog.azureml.ms/asset-gallery/v1.0/${modelFromIndex.registryName}/models/${modelFromIndex.name}/version/${modelFromIndex.version}`, 49 | ); 50 | if (!modelRes.ok) { 51 | throw new Error(`Failed to fetch ${modelName} details from the model catalog.`); 52 | } 53 | const model = (await modelRes.json()) as Model; 54 | return model; 55 | } 56 | 57 | async getModelSchema(modelName: string): Promise { 58 | const modelFromIndex = await this.getModelFromIndex(modelName); 59 | 60 | const modelSchemaRes = await fetch( 61 | `https://modelcatalogcachev2-ebendjczf0c5dzca.b02.azurefd.net/widgets/en/Serverless/${modelFromIndex.registryName.toLowerCase()}/${modelFromIndex.name.toLowerCase()}.json` 62 | ); 63 | if (!modelSchemaRes.ok) { 64 | throw new Error( 65 | `Failed to fetch ${modelName} schema from the model catalog.` 66 | ); 67 | } 68 | const modelSchema = (await modelSchemaRes.json()) as ModelSchema; 69 | return modelSchema; 70 | } 71 | 72 | async listModels(): Promise { 73 | if (this._models) { 74 | return this._models; 75 | } 76 | 77 | const modelsRes = await fetch( 78 | "https://api.catalog.azureml.ms/asset-gallery/v1.0/models", 79 | { 80 | method: "POST", 81 | headers: { 82 | "Content-Type": "application/json", 83 | }, 84 | body: JSON.stringify({ 85 | filters: [ 86 | { field: "freePlayground", values: ["true"], operator: "eq" }, 87 | { field: "labels", values: ["latest"], operator: "eq" }, 88 | ], 89 | order: [{ field: "displayName", direction: "Asc" }], 90 | }), 91 | } 92 | ); 93 | if (!modelsRes.ok) { 94 | throw new Error("Failed to fetch models from the model catalog"); 95 | } 96 | 97 | const models = (await modelsRes.json()).summaries as Model[]; 98 | this._models = models; 99 | return models; 100 | } 101 | 102 | async getModelFromIndex(modelName: string): Promise { 103 | this._models = this._models || (await this.listModels()); 104 | const modelFromIndex = this._models.find((model) => model.name === modelName); 105 | if (!modelFromIndex) { 106 | throw new Error(`Failed to fetch ${modelName} from the model catalog.`); 107 | } 108 | return modelFromIndex; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "NodeNext", 5 | "moduleResolution": "NodeNext", 6 | "rootDir": "./src", 7 | "outDir": "./dist", 8 | "esModuleInterop": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "strict": true, 11 | "skipLibCheck": true 12 | } 13 | } 14 | --------------------------------------------------------------------------------