From 118ccc1e0b6abdacad11f6e89526eb60628b8347 Mon Sep 17 00:00:00 2001 From: Erick Christian Date: Sun, 9 Mar 2025 23:15:44 +0700 Subject: [PATCH 01/30] docs: Add roadmap to README --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index bd4a7164..d57fb89a 100644 --- a/README.md +++ b/README.md @@ -76,3 +76,9 @@ bun run start - Rarely use architect mode - Do not enable automatic yes in aider config - Claude 3.7 thinking mode uses more tokens. Use it sparingly + +## Roadmap + +- Manual approval for every request +- Rate limiting (only allow request every X seconds) +- Token counting From dccb7b7ef1a36dd3909fcc83f7047a332dbce1e4 Mon Sep 17 00:00:00 2001 From: Erick Christian Date: Sun, 9 Mar 2025 23:24:29 +0700 Subject: [PATCH 02/30] refactor: Remove unused log paths and simplify server initialization --- src/lib/paths.ts | 4 ---- src/main.ts | 12 ++++++------ 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/src/lib/paths.ts b/src/lib/paths.ts index 83bd25fc..6dfd0134 100644 --- a/src/lib/paths.ts +++ b/src/lib/paths.ts @@ -4,12 +4,8 @@ import path from "pathe" const APP_DIR = path.join(os.homedir(), ".local", "share", "copilot-api") const GITHUB_TOKEN_PATH = path.join(APP_DIR, "github_token") -const LOG_PATH = path.join(APP_DIR, "logs") -const LOG_FILE = path.join(LOG_PATH, "app.log") export const PATHS = { APP_DIR, GITHUB_TOKEN_PATH, - LOG_PATH, - LOG_FILE, } diff --git a/src/main.ts b/src/main.ts index 99cca2d9..073ba05f 100644 --- a/src/main.ts +++ b/src/main.ts @@ -6,30 +6,30 @@ import { serve, type ServerHandler } from "srvx" import { initializeApp } from "./lib/initialization" import { logger } from "./lib/logger" -import { initializePort } from "./lib/port" import { server } from "./server" -export async function runServer(options: { +interface RunServerOptions { port: number verbose: boolean logFile?: string -}): Promise { +} + +export async function runServer(options: RunServerOptions): Promise { if (options.verbose) { consola.level = 5 consola.info("Verbose logging enabled") } - const port = await initializePort(options.port) await logger.initialize(options.logFile) await initializeApp() - const serverUrl = `http://localhost:${port}` + const serverUrl = `http://localhost:${options.port}` consola.box(`Server started at ${serverUrl}`) serve({ fetch: server.fetch as ServerHandler, - port, + port: options.port, }) } From 107c528a7c7b24d324b3557d51dd28418855be35 Mon Sep 17 00:00:00 2001 From: Erick Christian Date: Sun, 9 Mar 2025 23:35:05 +0700 Subject: [PATCH 03/30] fix: Ensure GitHub token file exists and has correct permissions --- src/lib/initialization.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/lib/initialization.ts b/src/lib/initialization.ts index c421b0f2..f6cc4f3b 100644 --- a/src/lib/initialization.ts +++ b/src/lib/initialization.ts @@ -9,10 +9,14 @@ import { getGitHubUser } from "~/services/github/get-user/service" import { getModels } from "../services/copilot/get-models/service" import { getGitHubToken } from "../services/github/get-token/service" -// Extract to individual functions for each initialization step async function initializeAppDirectory(): Promise { await fs.mkdir(PATHS.APP_DIR, { recursive: true }) - await fs.writeFile(PATHS.GITHUB_TOKEN_PATH, "", { flag: "a" }) + try { + await fs.access(PATHS.GITHUB_TOKEN_PATH, fs.constants.W_OK) + } catch { + await fs.writeFile(PATHS.GITHUB_TOKEN_PATH, "") + await fs.chmod(PATHS.GITHUB_TOKEN_PATH, 0o600) + } } async function initializeGithubAuthentication(): Promise { From 2d925b17c63c530f7df959c50b52a2db37a95a4d Mon Sep 17 00:00:00 2001 From: Erick Christian Date: Mon, 10 Mar 2025 05:35:07 +0700 Subject: [PATCH 04/30] refactor: use native fetch for github device flow --- src/lib/constants.ts | 13 ++++- src/lib/initialization.ts | 3 +- src/lib/sleep.ts | 4 ++ src/lib/state.ts | 9 ++++ src/services/github/get-token/service.ts | 62 ++++++++++++++---------- 5 files changed, 61 insertions(+), 30 deletions(-) create mode 100644 src/lib/sleep.ts create mode 100644 src/lib/state.ts diff --git a/src/lib/constants.ts b/src/lib/constants.ts index bf749d96..3b443045 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -1,5 +1,4 @@ // VSCode client ID -const GITHUB_CLIENT_ID = "01ab8ac9400c4e429b23" const GITHUB_OAUTH_SCOPES = [ "read:org", "read:user", @@ -9,7 +8,7 @@ const GITHUB_OAUTH_SCOPES = [ ].join(" ") export const ENV = { - GITHUB_CLIENT_ID, + GITHUB_CLIENT_ID: "01ab8ac9400c4e429b23", GITHUB_OAUTH_SCOPES, } @@ -29,3 +28,13 @@ export const GITHUB_API_CONFIG = { export const GITHUB_WEB_API_CONFIG = { baseURL: "https://github.com", } as const + +export const GITHUB_BASE_URL = "https://github.com" +export const GITHUB_CLIENT_ID = "01ab8ac9400c4e429b23" +export const GITHUB_APP_SCOPES = [ + "read:org", + "read:user", + "repo", + "user:email", + "workflow", +].join(" ") diff --git a/src/lib/initialization.ts b/src/lib/initialization.ts index f6cc4f3b..8e7570b1 100644 --- a/src/lib/initialization.ts +++ b/src/lib/initialization.ts @@ -62,8 +62,7 @@ async function logModelInformation(): Promise { async function initializeGithubToken() { consola.start("Getting GitHub device code") - const tokenResponse = await getGitHubToken() - return tokenResponse.access_token + return await getGitHubToken() } async function logUser() { diff --git a/src/lib/sleep.ts b/src/lib/sleep.ts new file mode 100644 index 00000000..35b2fd53 --- /dev/null +++ b/src/lib/sleep.ts @@ -0,0 +1,4 @@ +export const sleep = (ms: number) => + new Promise((resolve) => { + setTimeout(resolve, ms) + }) diff --git a/src/lib/state.ts b/src/lib/state.ts new file mode 100644 index 00000000..c0cfae6a --- /dev/null +++ b/src/lib/state.ts @@ -0,0 +1,9 @@ +import type { GetModelsResponse } from "~/services/copilot/get-models/types" + +interface State { + githubToken?: string + copilotToken?: string + models?: GetModelsResponse +} + +export const state: State = {} diff --git a/src/services/github/get-token/service.ts b/src/services/github/get-token/service.ts index a126b37c..6fdb3df7 100644 --- a/src/services/github/get-token/service.ts +++ b/src/services/github/get-token/service.ts @@ -1,7 +1,7 @@ +import { sleep } from "bun" import consola from "consola" -import { ENV } from "~/lib/constants" -import { _github } from "~/services/api-instance" +import { GITHUB_CLIENT_ID, GITHUB_BASE_URL } from "~/lib/constants" interface DeviceCodeResponse { device_code: string @@ -18,42 +18,52 @@ interface AccessTokenResponse { } export async function getGitHubToken() { - const response = await _github("/login/device/code", { + const response = await fetch(`${GITHUB_BASE_URL}/login/device/code`, { method: "POST", - body: { - client_id: ENV.GITHUB_CLIENT_ID, - scope: ENV.GITHUB_OAUTH_SCOPES, - }, + body: JSON.stringify({ + client_id: GITHUB_CLIENT_ID, + }), }) - consola.info( - `Please enter the code "${response.user_code}" in ${response.verification_uri}`, - ) + if (!response.ok) { + throw new Error("Failed to get device code", { + cause: await response.json(), + }) + } + + const { user_code, verification_uri, device_code, interval } = + (await response.json()) as DeviceCodeResponse + + consola.info(`Please enter the code "${user_code}" in ${verification_uri}`) while (true) { - const pollResponse = await _github( - "/login/oauth/access_token", + const response = await fetch( + `${GITHUB_BASE_URL}/login/oauth/access_token`, { method: "POST", - body: { - client_id: ENV.GITHUB_CLIENT_ID, - device_code: response.device_code, + body: JSON.stringify({ + client_id: GITHUB_CLIENT_ID, + device_code, grant_type: "urn:ietf:params:oauth:grant-type:device_code", - }, + }), }, ) - if (pollResponse.access_token) { - consola.info( - `Got token ${pollResponse.access_token.replaceAll(/./g, "*")}`, - ) - return pollResponse + // Interval is in seconds, we need to multiply by 1000 to get milliseconds + // I'm also adding another second, just to be safe + const sleepDuration = (interval + 1) * 1000 + + if (!response.ok) { + await sleep(sleepDuration) + continue + } + + const { access_token } = (await response.json()) as AccessTokenResponse + + if (access_token) { + return access_token } else { - // Interval is in seconds, we need to multiply by 1000 to get milliseconds - // I'm also adding another second, just to be safe - await new Promise((resolve) => - setTimeout(resolve, (response.interval + 1) * 1000), - ) + await sleep(sleepDuration) } } } From e7e9dafbfaedad8e240c8115959848456cec0a65 Mon Sep 17 00:00:00 2001 From: Erick Christian Date: Mon, 10 Mar 2025 18:35:21 +0700 Subject: [PATCH 05/30] refactor: extract token retrieval into smaller functions --- src/lib/paths.ts | 17 +++++- src/services/github/get-device-code.ts | 26 +++++++++ src/services/github/get-token/service.ts | 69 +++--------------------- src/services/github/poll-access-token.ts | 45 ++++++++++++++++ 4 files changed, 94 insertions(+), 63 deletions(-) create mode 100644 src/services/github/get-device-code.ts create mode 100644 src/services/github/poll-access-token.ts diff --git a/src/lib/paths.ts b/src/lib/paths.ts index 6dfd0134..8d0a9f02 100644 --- a/src/lib/paths.ts +++ b/src/lib/paths.ts @@ -1,5 +1,6 @@ +import fs from "node:fs/promises" import os from "node:os" -import path from "pathe" +import path from "node:path" const APP_DIR = path.join(os.homedir(), ".local", "share", "copilot-api") @@ -9,3 +10,17 @@ export const PATHS = { APP_DIR, GITHUB_TOKEN_PATH, } + +export async function ensurePaths(): Promise { + await fs.mkdir(PATHS.APP_DIR, { recursive: true }) + await ensureFile(PATHS.GITHUB_TOKEN_PATH) +} + +async function ensureFile(filePath: string): Promise { + try { + await fs.access(filePath, fs.constants.W_OK) + } catch { + await fs.writeFile(filePath, "") + await fs.chmod(filePath, 0o600) + } +} diff --git a/src/services/github/get-device-code.ts b/src/services/github/get-device-code.ts new file mode 100644 index 00000000..57c969d3 --- /dev/null +++ b/src/services/github/get-device-code.ts @@ -0,0 +1,26 @@ +import { GITHUB_BASE_URL, GITHUB_CLIENT_ID } from "~/lib/constants" + +export async function getDeviceCode(): Promise { + const response = await fetch(`${GITHUB_BASE_URL}/login/device/code`, { + method: "POST", + body: JSON.stringify({ + client_id: GITHUB_CLIENT_ID, + }), + }) + + if (!response.ok) { + throw new Error("Failed to get device code", { + cause: await response.json(), + }) + } + + return (await response.json()) as DeviceCodeResponse +} + +export interface DeviceCodeResponse { + device_code: string + user_code: string + verification_uri: string + expires_in: number + interval: number +} diff --git a/src/services/github/get-token/service.ts b/src/services/github/get-token/service.ts index 6fdb3df7..31fd48e8 100644 --- a/src/services/github/get-token/service.ts +++ b/src/services/github/get-token/service.ts @@ -1,69 +1,14 @@ -import { sleep } from "bun" import consola from "consola" -import { GITHUB_CLIENT_ID, GITHUB_BASE_URL } from "~/lib/constants" - -interface DeviceCodeResponse { - device_code: string - user_code: string - verification_uri: string - expires_in: number - interval: number -} - -interface AccessTokenResponse { - access_token: string - token_type: string - scope: string -} +import { getDeviceCode } from "../get-device-code" +import { pollAccessToken } from "../poll-access-token" export async function getGitHubToken() { - const response = await fetch(`${GITHUB_BASE_URL}/login/device/code`, { - method: "POST", - body: JSON.stringify({ - client_id: GITHUB_CLIENT_ID, - }), - }) - - if (!response.ok) { - throw new Error("Failed to get device code", { - cause: await response.json(), - }) - } - - const { user_code, verification_uri, device_code, interval } = - (await response.json()) as DeviceCodeResponse - - consola.info(`Please enter the code "${user_code}" in ${verification_uri}`) - - while (true) { - const response = await fetch( - `${GITHUB_BASE_URL}/login/oauth/access_token`, - { - method: "POST", - body: JSON.stringify({ - client_id: GITHUB_CLIENT_ID, - device_code, - grant_type: "urn:ietf:params:oauth:grant-type:device_code", - }), - }, - ) - - // Interval is in seconds, we need to multiply by 1000 to get milliseconds - // I'm also adding another second, just to be safe - const sleepDuration = (interval + 1) * 1000 - - if (!response.ok) { - await sleep(sleepDuration) - continue - } + const response = await getDeviceCode() - const { access_token } = (await response.json()) as AccessTokenResponse + consola.info( + `Please enter the code "${response.user_code}" in ${response.verification_uri}`, + ) - if (access_token) { - return access_token - } else { - await sleep(sleepDuration) - } - } + return await pollAccessToken(response) } diff --git a/src/services/github/poll-access-token.ts b/src/services/github/poll-access-token.ts new file mode 100644 index 00000000..17d0b32f --- /dev/null +++ b/src/services/github/poll-access-token.ts @@ -0,0 +1,45 @@ +import { GITHUB_BASE_URL, GITHUB_CLIENT_ID } from "~/lib/constants" +import { sleep } from "~/lib/sleep" + +import type { DeviceCodeResponse } from "./get-device-code" + +export async function pollAccessToken( + deviceCode: DeviceCodeResponse, +): Promise { + while (true) { + const response = await fetch( + `${GITHUB_BASE_URL}/login/oauth/access_token`, + { + method: "POST", + body: JSON.stringify({ + client_id: GITHUB_CLIENT_ID, + device_code: deviceCode.device_code, + grant_type: "urn:ietf:params:oauth:grant-type:device_code", + }), + }, + ) + + // Interval is in seconds, we need to multiply by 1000 to get milliseconds + // I'm also adding another second, just to be safe + const sleepDuration = (deviceCode.interval + 1) * 1000 + + if (!response.ok) { + await sleep(sleepDuration) + continue + } + + const { access_token } = (await response.json()) as AccessTokenResponse + + if (access_token) { + return access_token + } else { + await sleep(sleepDuration) + } + } +} + +interface AccessTokenResponse { + access_token: string + token_type: string + scope: string +} From ac7219f45b9f9940639a56be9e9d7fbb2c776554 Mon Sep 17 00:00:00 2001 From: Erick Christian Date: Mon, 10 Mar 2025 18:53:26 +0700 Subject: [PATCH 06/30] feat: Improve initialization and update build config --- src/lib/initialization.ts | 15 ++------------- tsup.config.ts | 8 +++----- 2 files changed, 5 insertions(+), 18 deletions(-) diff --git a/src/lib/initialization.ts b/src/lib/initialization.ts index 8e7570b1..035588b9 100644 --- a/src/lib/initialization.ts +++ b/src/lib/initialization.ts @@ -1,24 +1,13 @@ import consola from "consola" -import fs from "node:fs/promises" import { FetchError } from "ofetch" -import { PATHS } from "~/lib/paths" +import { ensurePaths } from "~/lib/paths" import { tokenService } from "~/lib/token" import { getGitHubUser } from "~/services/github/get-user/service" import { getModels } from "../services/copilot/get-models/service" import { getGitHubToken } from "../services/github/get-token/service" -async function initializeAppDirectory(): Promise { - await fs.mkdir(PATHS.APP_DIR, { recursive: true }) - try { - await fs.access(PATHS.GITHUB_TOKEN_PATH, fs.constants.W_OK) - } catch { - await fs.writeFile(PATHS.GITHUB_TOKEN_PATH, "") - await fs.chmod(PATHS.GITHUB_TOKEN_PATH, 0o600) - } -} - async function initializeGithubAuthentication(): Promise { const githubToken = await tokenService.getGithubToken() @@ -71,7 +60,7 @@ async function logUser() { } export async function initializeApp() { - await initializeAppDirectory() + await ensurePaths() await initializeGithubAuthentication() await initializeCopilotToken() await logModelInformation() diff --git a/tsup.config.ts b/tsup.config.ts index 22ecbd8c..4e007873 100644 --- a/tsup.config.ts +++ b/tsup.config.ts @@ -4,14 +4,12 @@ export default defineConfig({ entry: ["src/main.ts"], format: ["esm"], - target: "esnext", + target: "es2022", platform: "node", - dts: true, - removeNodeProtocol: false, - sourcemap: true, - shims: true, + minify: true, clean: true, + removeNodeProtocol: false, env: { NODE_ENV: "production", From 9c457c573c9e4b60a7d95f344672bf6b416b59fa Mon Sep 17 00:00:00 2001 From: Erick Christian Date: Tue, 11 Mar 2025 08:05:51 +0700 Subject: [PATCH 07/30] chore: Update srvx dependency and remove vitest-related packages --- bun.lock | 64 ++---------------------------------- package.json | 6 ++-- test/main.test.ts | 84 ----------------------------------------------- 3 files changed, 5 insertions(+), 149 deletions(-) delete mode 100644 test/main.test.ts diff --git a/bun.lock b/bun.lock index 2abe97cc..62e44bbc 100644 --- a/bun.lock +++ b/bun.lock @@ -11,7 +11,7 @@ "hono": "^4.7.4", "ofetch": "^1.4.1", "pathe": "^2.0.3", - "srvx": "^0.1.4", + "srvx": "^0.2.5", "zod": "^3.24.2", }, "devDependencies": { @@ -22,10 +22,8 @@ "knip": "^5.45.0", "lint-staged": "^15.4.3", "simple-git-hooks": "^2.11.1", - "tinyexec": "^0.3.2", "tsup": "^8.4.0", "typescript": "^5.8.2", - "vitest": "^3.0.8", }, }, }, @@ -226,20 +224,6 @@ "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.26.0", "", { "dependencies": { "@typescript-eslint/types": "8.26.0", "eslint-visitor-keys": "^4.2.0" } }, "sha512-2z8JQJWAzPdDd51dRQ/oqIJxe99/hoLIqmf8RMCAJQtYDc535W/Jt2+RTP4bP0aKeBG1F65yjIZuczOXCmbWwg=="], - "@vitest/expect": ["@vitest/expect@3.0.8", "", { "dependencies": { "@vitest/spy": "3.0.8", "@vitest/utils": "3.0.8", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-Xu6TTIavTvSSS6LZaA3EebWFr6tsoXPetOWNMOlc7LO88QVVBwq2oQWBoDiLCN6YTvNYsGSjqOO8CAdjom5DCQ=="], - - "@vitest/mocker": ["@vitest/mocker@3.0.8", "", { "dependencies": { "@vitest/spy": "3.0.8", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^5.0.0 || ^6.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-n3LjS7fcW1BCoF+zWZxG7/5XvuYH+lsFg+BDwwAz0arIwHQJFUEsKBQ0BLU49fCxuM/2HSeBPHQD8WjgrxMfow=="], - - "@vitest/pretty-format": ["@vitest/pretty-format@3.0.8", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-BNqwbEyitFhzYMYHUVbIvepOyeQOSFA/NeJMIP9enMntkkxLgOcgABH6fjyXG85ipTgvero6noreavGIqfJcIg=="], - - "@vitest/runner": ["@vitest/runner@3.0.8", "", { "dependencies": { "@vitest/utils": "3.0.8", "pathe": "^2.0.3" } }, "sha512-c7UUw6gEcOzI8fih+uaAXS5DwjlBaCJUo7KJ4VvJcjL95+DSR1kova2hFuRt3w41KZEFcOEiq098KkyrjXeM5w=="], - - "@vitest/snapshot": ["@vitest/snapshot@3.0.8", "", { "dependencies": { "@vitest/pretty-format": "3.0.8", "magic-string": "^0.30.17", "pathe": "^2.0.3" } }, "sha512-x8IlMGSEMugakInj44nUrLSILh/zy1f2/BgH0UeHpNyOocG18M9CWVIFBaXPt8TrqVZWmcPjwfG/ht5tnpba8A=="], - - "@vitest/spy": ["@vitest/spy@3.0.8", "", { "dependencies": { "tinyspy": "^3.0.2" } }, "sha512-MR+PzJa+22vFKYb934CejhR4BeRpMSoxkvNoDit68GQxRLSf11aT6CTj3XaqUU9rxgWJFnqicN/wxw6yBRkI1Q=="], - - "@vitest/utils": ["@vitest/utils@3.0.8", "", { "dependencies": { "@vitest/pretty-format": "3.0.8", "loupe": "^3.1.3", "tinyrainbow": "^2.0.0" } }, "sha512-nkBC3aEhfX2PdtQI/QwAWp8qZWwzASsU4Npbcd5RdMPBSSLCpkZp52P3xku3s3uA0HIEhGvEcF8rNkBsz9dQ4Q=="], - "acorn": ["acorn@8.14.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA=="], "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], @@ -272,8 +256,6 @@ "arraybuffer.prototype.slice": ["arraybuffer.prototype.slice@1.0.4", "", { "dependencies": { "array-buffer-byte-length": "^1.0.1", "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "is-array-buffer": "^3.0.4" } }, "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ=="], - "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], - "ast-types-flow": ["ast-types-flow@0.0.8", "", {}, "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ=="], "async-function": ["async-function@1.0.0", "", {}, "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA=="], @@ -316,12 +298,8 @@ "caniuse-lite": ["caniuse-lite@1.0.30001702", "", {}, "sha512-LoPe/D7zioC0REI5W73PeR1e1MLCipRGq/VkovJnd6Df+QVqT+vT33OXCp8QUd7kA7RZrHWxb1B36OQKI/0gOA=="], - "chai": ["chai@5.2.0", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw=="], - "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], - "check-error": ["check-error@2.1.1", "", {}, "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw=="], - "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], "chownr": ["chownr@2.0.0", "", {}, "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ=="], @@ -360,7 +338,7 @@ "consola": ["consola@3.4.0", "", {}, "sha512-EiPU8G6dQG0GFHNR8ljnZFki/8a+cQwEQ+7wpxdChl02Q8HXlwEZWD5lqAF8vC2sEC3Tehr8hy7vErz88LHyUA=="], - "cookie-es": ["cookie-es@1.2.2", "", {}, "sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg=="], + "cookie-es": ["cookie-es@2.0.0", "", {}, "sha512-RAj4E421UYRgqokKUmotqAwuplYw15qtdXfY+hGzgCJ/MBjCVZcSoHK/kH9kocfjRjcDME7IiDWR/1WX1TM2Pg=="], "core-js-compat": ["core-js-compat@3.41.0", "", { "dependencies": { "browserslist": "^4.24.4" } }, "sha512-RFsU9LySVue9RTwdDVX/T0e2Y6jRYWXERKElIjpuEOEnxaXffI0X7RUwVzfYLfzuLXSNJDYoRYUAmRUcyln20A=="], @@ -376,8 +354,6 @@ "debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], - "deep-eql": ["deep-eql@5.0.2", "", {}, "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q=="], - "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], "defaults": ["defaults@1.0.4", "", { "dependencies": { "clone": "^1.0.2" } }, "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A=="], @@ -416,8 +392,6 @@ "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], - "es-module-lexer": ["es-module-lexer@1.6.0", "", {}, "sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ=="], - "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], @@ -482,16 +456,12 @@ "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], - "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], - "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], "eventemitter3": ["eventemitter3@5.0.1", "", {}, "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="], "execa": ["execa@8.0.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^8.0.1", "human-signals": "^5.0.0", "is-stream": "^3.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^5.1.0", "onetime": "^6.0.0", "signal-exit": "^4.1.0", "strip-final-newline": "^3.0.0" } }, "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg=="], - "expect-type": ["expect-type@1.2.0", "", {}, "sha512-80F22aiJ3GLyVnS/B3HzgR6RelZVumzj9jkL0Rhz4h0xYbNW9PjlQz5h3J/SShErbXBc295vseR4/MIbVmUbeA=="], - "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], "fast-diff": ["fast-diff@1.3.0", "", {}, "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw=="], @@ -714,12 +684,8 @@ "log-update": ["log-update@6.1.0", "", { "dependencies": { "ansi-escapes": "^7.0.0", "cli-cursor": "^5.0.0", "slice-ansi": "^7.1.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w=="], - "loupe": ["loupe@3.1.3", "", {}, "sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug=="], - "lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], - "magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="], - "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], "merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="], @@ -814,8 +780,6 @@ "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], - "pathval": ["pathval@2.0.0", "", {}, "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA=="], - "perfect-debounce": ["perfect-debounce@1.0.0", "", {}, "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA=="], "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], @@ -912,8 +876,6 @@ "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], - "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], - "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], "simple-git-hooks": ["simple-git-hooks@2.11.1", "", { "bin": { "simple-git-hooks": "cli.js" } }, "sha512-tgqwPUMDcNDhuf1Xf6KTUsyeqGdgKMhzaH4PAZZuzguOgTl5uuyeYe/8mWgAr6IBxB5V06uqEf6Dy37gIWDtDg=="], @@ -940,11 +902,7 @@ "spdx-license-ids": ["spdx-license-ids@3.0.21", "", {}, "sha512-Bvg/8F5XephndSK3JffaRqdT+gyhfqIPwDHpX80tJrF8QQRYMo8sNMeaZ2Dp5+jhwKnUmIOyFFQfHRkjJm5nXg=="], - "srvx": ["srvx@0.1.4", "", { "dependencies": { "cookie-es": "^1.2.2" } }, "sha512-hHt1/s+3o4tOOjC2YCr7bwi4msAXYJYErVpz2w/FcvG3ODRV0GZsdHsBjeKqY46psZmRbItfPLMp2oP7JsZaow=="], - - "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], - - "std-env": ["std-env@3.8.0", "", {}, "sha512-Bc3YwwCB+OzldMxOXJIIvC6cPRWr/LxOp48CdQTOkPyk/t4JWWJbrilwBd7RJzKV8QW7tJkcgAmeuLLJugl5/w=="], + "srvx": ["srvx@0.2.5", "", { "dependencies": { "cookie-es": "^2.0.0" } }, "sha512-G63uf9Emf8PQPlWkBKFfcqTkVjwIF5Z8lfECidSiaAXrd19Pj6ijU676yRfYP3KShZY7KmLsfb4/unIOCtnWfA=="], "string-argv": ["string-argv@0.3.2", "", {}, "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q=="], @@ -988,18 +946,10 @@ "thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="], - "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], - "tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="], "tinyglobby": ["tinyglobby@0.2.10", "", { "dependencies": { "fdir": "^6.4.2", "picomatch": "^4.0.2" } }, "sha512-Zc+8eJlFMvgatPZTl6A9L/yht8QqdmUNtURHaKZLmKBE12hNPSrqNkUp2cs3M/UKmNVVAMFQYSjYIVHDjW5zew=="], - "tinypool": ["tinypool@1.0.2", "", {}, "sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA=="], - - "tinyrainbow": ["tinyrainbow@2.0.0", "", {}, "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw=="], - - "tinyspy": ["tinyspy@3.0.2", "", {}, "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q=="], - "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], "tr46": ["tr46@1.0.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA=="], @@ -1048,12 +998,6 @@ "validate-npm-package-name": ["validate-npm-package-name@6.0.0", "", {}, "sha512-d7KLgL1LD3U3fgnvWEY1cQXoO/q6EQ1BSz48Sa149V/5zVTAbgmZIpyI8TRi6U9/JNyeYLlTKsEMPtLC27RFUg=="], - "vite": ["vite@6.2.0", "", { "dependencies": { "esbuild": "^0.25.0", "postcss": "^8.5.3", "rollup": "^4.30.1" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-7dPxoo+WsT/64rDcwoOjk76XHj+TqNTIvHKcuMQ1k4/SeHDaQt5GFAeLYzrimZrMpn/O6DtdI03WUjdxuPM0oQ=="], - - "vite-node": ["vite-node@3.0.8", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.4.0", "es-module-lexer": "^1.6.0", "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-6PhR4H9VGlcwXZ+KWCdMqbtG649xCPZqfI9j2PsK1FcXgEzro5bGHcVKFCTqPLaNKZES8Evqv4LwvZARsq5qlg=="], - - "vitest": ["vitest@3.0.8", "", { "dependencies": { "@vitest/expect": "3.0.8", "@vitest/mocker": "3.0.8", "@vitest/pretty-format": "^3.0.8", "@vitest/runner": "3.0.8", "@vitest/snapshot": "3.0.8", "@vitest/spy": "3.0.8", "@vitest/utils": "3.0.8", "chai": "^5.2.0", "debug": "^4.4.0", "expect-type": "^1.1.0", "magic-string": "^0.30.17", "pathe": "^2.0.3", "std-env": "^3.8.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", "tinypool": "^1.0.2", "tinyrainbow": "^2.0.0", "vite": "^5.0.0 || ^6.0.0", "vite-node": "3.0.8", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "@vitest/browser": "3.0.8", "@vitest/ui": "3.0.8", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@types/debug", "@types/node", "@vitest/browser", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-dfqAsNqRGUc8hB9OVR2P0w8PZPEckti2+5rdZip0WIz9WW0MnImJ8XiR61QhqLa92EQzKP2uPkzenKOAHyEIbA=="], - "wcwidth": ["wcwidth@1.0.1", "", { "dependencies": { "defaults": "^1.0.3" } }, "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg=="], "webidl-conversions": ["webidl-conversions@4.0.2", "", {}, "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg=="], @@ -1070,8 +1014,6 @@ "which-typed-array": ["which-typed-array@1.1.19", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw=="], - "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], - "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], "wrap-ansi": ["wrap-ansi@9.0.0", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q=="], diff --git a/package.json b/package.json index fd046d35..e38c2626 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "hono": "^4.7.4", "ofetch": "^1.4.1", "pathe": "^2.0.3", - "srvx": "^0.1.4", + "srvx": "^0.2.5", "zod": "^3.24.2" }, "devDependencies": { @@ -57,9 +57,7 @@ "knip": "^5.45.0", "lint-staged": "^15.4.3", "simple-git-hooks": "^2.11.1", - "tinyexec": "^0.3.2", "tsup": "^8.4.0", - "typescript": "^5.8.2", - "vitest": "^3.0.8" + "typescript": "^5.8.2" } } diff --git a/test/main.test.ts b/test/main.test.ts deleted file mode 100644 index 52d7826d..00000000 --- a/test/main.test.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { stream } from "fetch-event-stream" -import { x } from "tinyexec" -import { describe, it, beforeAll, afterAll, expect } from "vitest" - -import type { ChatCompletionsPayload } from "../src/services/copilot/chat-completions/types" - -import { ChatCompletionChunkSchema } from "../src/services/copilot/chat-completions/types-streaming" - -describe("Server API Tests", () => { - const TEST_PORT = 4142 - const BASE_URL = `http://localhost:${TEST_PORT}` - - let serverProcess: ReturnType - - beforeAll(async () => { - // Start the server as a separate process - serverProcess = x("bun", ["run", "start", "--port", TEST_PORT.toString()]) - - // Wait a bit for server to be ready - await new Promise((resolve) => setTimeout(resolve, 5000)) - }) - - afterAll(() => { - serverProcess.kill("SIGTERM") - }) - - it("POST /chat/completions should return valid completion (streaming)", async () => { - const payload: ChatCompletionsPayload = { - messages: [{ role: "user", content: "Write a short greeting" }], - model: "gpt-3.5-turbo", - stream: true, // Make sure to set stream to true - } - - let receivedChunks = 0 - let hasContent = false - let hasFinishReason = false - - try { - const response = await stream(`${BASE_URL}/chat/completions`, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(payload), - }) - - for await (const chunk of response) { - console.log(chunk) - - if (chunk.data === "[DONE]") break - - // Validate each chunk against our schema - const parseResult = ChatCompletionChunkSchema.safeParse( - JSON.parse(chunk.data ?? "{}"), - ) - - if (!parseResult.success) { - console.error("Invalid chunk format:", parseResult.error) - throw new Error(`Invalid chunk format: ${parseResult.error.message}`) - } - - receivedChunks++ - - // Check if we're getting content in the delta - if (parseResult.data.choices[0]?.delta?.content) { - hasContent = true - } - - // Check if we get a finish reason (indicates completion) - if (parseResult.data.choices[0]?.finish_reason) { - hasFinishReason = true - } - } - - // Add assertions to verify the response was correct - expect(receivedChunks).toBeGreaterThan(0) - expect(hasContent).toBe(true) - expect(hasFinishReason).toBe(true) - } catch (error) { - console.error("Streaming test failed:", error) - throw error - } - }) -}) From 06656610f1b2b9f31661bb52d93ed783b4ac0809 Mon Sep 17 00:00:00 2001 From: Erick Christian Date: Tue, 11 Mar 2025 08:25:34 +0700 Subject: [PATCH 08/30] refactor: simplify GitHub authentication and token handling --- src/lib/initialization.ts | 66 +++++++++++++++++---------------------- src/lib/token.ts | 45 +++++++++++++++----------- 2 files changed, 54 insertions(+), 57 deletions(-) diff --git a/src/lib/initialization.ts b/src/lib/initialization.ts index 035588b9..f3602d65 100644 --- a/src/lib/initialization.ts +++ b/src/lib/initialization.ts @@ -1,41 +1,41 @@ import consola from "consola" -import { FetchError } from "ofetch" import { ensurePaths } from "~/lib/paths" -import { tokenService } from "~/lib/token" +import { readGithubToken, tokenService, writeGithubToken } from "~/lib/token" +import { getDeviceCode } from "~/services/github/get-device-code" import { getGitHubUser } from "~/services/github/get-user/service" +import { pollAccessToken } from "~/services/github/poll-access-token" import { getModels } from "../services/copilot/get-models/service" -import { getGitHubToken } from "../services/github/get-token/service" +import { state } from "./state" -async function initializeGithubAuthentication(): Promise { - const githubToken = await tokenService.getGithubToken() +async function logUser() { + const user = await getGitHubUser() + consola.info(`Logged in as ${JSON.stringify(user.login)}\n`) +} - try { - if (githubToken) { - // Set token in the service so github fetcher can use it - await tokenService.setGithubToken(githubToken) - await logUser() - } else { - throw new Error("No GitHub token available") - } - } catch (error) { - if (error instanceof FetchError && error.statusCode !== 401) { - consola.error("Authentication error:", { - error, - request: error.request, - options: error.options, - response: error.response, - data: error.response?._data as Record, - }) - throw error - } +async function setupGitHubToken(): Promise { + const githubToken = await readGithubToken() - consola.info("Not logged in, getting new access token") - const newToken = await initializeGithubToken() - await tokenService.setGithubToken(newToken) + if (githubToken) { + state.githubToken = githubToken await logUser() + + return } + + consola.info("Not logged in, getting new access token") + const response = await getDeviceCode() + + consola.info( + `Please enter the code "${response.user_code}" in ${response.verification_uri}`, + ) + + const token = await pollAccessToken(response) + await writeGithubToken(token) + state.githubToken = token + + await logUser() } async function initializeCopilotToken(): Promise { @@ -49,19 +49,9 @@ async function logModelInformation(): Promise { ) } -async function initializeGithubToken() { - consola.start("Getting GitHub device code") - return await getGitHubToken() -} - -async function logUser() { - const user = await getGitHubUser() - consola.info(`Logged in as ${JSON.stringify(user.login)}\n`) -} - export async function initializeApp() { await ensurePaths() - await initializeGithubAuthentication() + await setupGitHubToken() await initializeCopilotToken() await logModelInformation() } diff --git a/src/lib/token.ts b/src/lib/token.ts index c3c34b09..342bd8c6 100644 --- a/src/lib/token.ts +++ b/src/lib/token.ts @@ -4,6 +4,32 @@ import fs from "node:fs/promises" import { PATHS } from "~/lib/paths" import { getCopilotToken } from "~/services/copilot/get-token/copilot-token" +import { state } from "./state" + +export const readGithubToken = () => + fs.readFile(PATHS.GITHUB_TOKEN_PATH, "utf8") + +export const writeGithubToken = (token: string) => + fs.writeFile(PATHS.GITHUB_TOKEN_PATH, token) + +export const setupCopilotTokenRefresh = async () => { + const { token, refresh_in } = await getCopilotToken() + state.copilotToken = token + + const refreshInterval = (refresh_in - 60) * 1000 + + setInterval(async () => { + consola.start("Refreshing Copilot token") + try { + const { token } = await getCopilotToken() + state.copilotToken = token + } catch (error) { + consola.error("Failed to refresh Copilot token:", error) + throw error + } + }, refreshInterval) +} + // Simple token manager with basic encapsulation export const tokenService = { // Private token storage @@ -12,25 +38,6 @@ export const tokenService = { copilot: undefined as string | undefined, }, - // Get GitHub token - async getGithubToken(): Promise { - if (!this._tokens.github) { - try { - this._tokens.github = await fs.readFile(PATHS.GITHUB_TOKEN_PATH, "utf8") - } catch (error) { - consola.warn("Failed to load GitHub token", error) - } - } - - return this._tokens.github - }, - - // Set GitHub token - async setGithubToken(token: string): Promise { - this._tokens.github = token - await fs.writeFile(PATHS.GITHUB_TOKEN_PATH, token) - }, - // Get Copilot token getCopilotToken(): string | undefined { return this._tokens.copilot From 5bb874137ae81e64d6e7099f72b27027037e82a9 Mon Sep 17 00:00:00 2001 From: Erick Christian Date: Tue, 11 Mar 2025 08:39:09 +0700 Subject: [PATCH 09/30] refactor: Move initialization logic to main and lib files --- src/lib/initialization.ts | 57 ------------------------------------- src/lib/models.ts | 15 ++++++++++ src/lib/token.ts | 60 ++++++++++++++++++--------------------- src/main.ts | 9 ++++-- 4 files changed, 49 insertions(+), 92 deletions(-) delete mode 100644 src/lib/initialization.ts diff --git a/src/lib/initialization.ts b/src/lib/initialization.ts deleted file mode 100644 index f3602d65..00000000 --- a/src/lib/initialization.ts +++ /dev/null @@ -1,57 +0,0 @@ -import consola from "consola" - -import { ensurePaths } from "~/lib/paths" -import { readGithubToken, tokenService, writeGithubToken } from "~/lib/token" -import { getDeviceCode } from "~/services/github/get-device-code" -import { getGitHubUser } from "~/services/github/get-user/service" -import { pollAccessToken } from "~/services/github/poll-access-token" - -import { getModels } from "../services/copilot/get-models/service" -import { state } from "./state" - -async function logUser() { - const user = await getGitHubUser() - consola.info(`Logged in as ${JSON.stringify(user.login)}\n`) -} - -async function setupGitHubToken(): Promise { - const githubToken = await readGithubToken() - - if (githubToken) { - state.githubToken = githubToken - await logUser() - - return - } - - consola.info("Not logged in, getting new access token") - const response = await getDeviceCode() - - consola.info( - `Please enter the code "${response.user_code}" in ${response.verification_uri}`, - ) - - const token = await pollAccessToken(response) - await writeGithubToken(token) - state.githubToken = token - - await logUser() -} - -async function initializeCopilotToken(): Promise { - await tokenService.initCopilotToken() -} - -async function logModelInformation(): Promise { - const models = await getModels() - consola.info( - `Available models: \n${models.data.map((model) => `- ${model.id}`).join("\n")}`, - ) -} - -export async function initializeApp() { - await ensurePaths() - await setupGitHubToken() - await initializeCopilotToken() - await logModelInformation() -} diff --git a/src/lib/models.ts b/src/lib/models.ts index 2f0ada3f..f6c779d9 100644 --- a/src/lib/models.ts +++ b/src/lib/models.ts @@ -1,5 +1,11 @@ +import consola from "consola" + import type { GetModelsResponse } from "~/services/copilot/get-models/types" +import { getModels } from "~/services/copilot/get-models/service" + +import { state } from "./state" + export const modelsCache = { _models: null as GetModelsResponse | null, @@ -11,3 +17,12 @@ export const modelsCache = { return this._models }, } + +export async function cacheModels(): Promise { + const models = await getModels() + state.models = models + + consola.info( + `Available models: \n${models.data.map((model) => `- ${model.id}`).join("\n")}`, + ) +} diff --git a/src/lib/token.ts b/src/lib/token.ts index 342bd8c6..e4d4f1f7 100644 --- a/src/lib/token.ts +++ b/src/lib/token.ts @@ -3,6 +3,9 @@ import fs from "node:fs/promises" import { PATHS } from "~/lib/paths" import { getCopilotToken } from "~/services/copilot/get-token/copilot-token" +import { getDeviceCode } from "~/services/github/get-device-code" +import { getGitHubUser } from "~/services/github/get-user/service" +import { pollAccessToken } from "~/services/github/poll-access-token" import { state } from "./state" @@ -12,7 +15,7 @@ export const readGithubToken = () => export const writeGithubToken = (token: string) => fs.writeFile(PATHS.GITHUB_TOKEN_PATH, token) -export const setupCopilotTokenRefresh = async () => { +export const setupCopilotToken = async () => { const { token, refresh_in } = await getCopilotToken() state.copilotToken = token @@ -30,40 +33,31 @@ export const setupCopilotTokenRefresh = async () => { }, refreshInterval) } -// Simple token manager with basic encapsulation -export const tokenService = { - // Private token storage - _tokens: { - github: undefined as string | undefined, - copilot: undefined as string | undefined, - }, +export async function setupGitHubToken(): Promise { + const githubToken = await readGithubToken() - // Get Copilot token - getCopilotToken(): string | undefined { - return this._tokens.copilot - }, + if (githubToken) { + state.githubToken = githubToken + await logUser() - // Set Copilot token - setCopilotToken(token: string): void { - this._tokens.copilot = token - }, + return + } - // Initialize Copilot token with auto-refresh - async initCopilotToken(): Promise { - const { token, refresh_in } = await getCopilotToken() - this.setCopilotToken(token) + consola.info("Not logged in, getting new access token") + const response = await getDeviceCode() - // Set up refresh timer - const refreshInterval = (refresh_in - 60) * 1000 - setInterval(async () => { - consola.start("Refreshing Copilot token") - try { - const { token: newToken } = await getCopilotToken() - this.setCopilotToken(newToken) - consola.success("Copilot token refreshed") - } catch (error) { - consola.error("Failed to refresh Copilot token:", error) - } - }, refreshInterval) - }, + consola.info( + `Please enter the code "${response.user_code}" in ${response.verification_uri}`, + ) + + const token = await pollAccessToken(response) + await writeGithubToken(token) + state.githubToken = token + + await logUser() +} + +async function logUser() { + const user = await getGitHubUser() + consola.info(`Logged in as ${JSON.stringify(user.login)}\n`) } diff --git a/src/main.ts b/src/main.ts index 073ba05f..498a019a 100644 --- a/src/main.ts +++ b/src/main.ts @@ -4,8 +4,10 @@ import { defineCommand, runMain } from "citty" import consola from "consola" import { serve, type ServerHandler } from "srvx" -import { initializeApp } from "./lib/initialization" import { logger } from "./lib/logger" +import { cacheModels } from "./lib/models" +import { ensurePaths } from "./lib/paths" +import { setupCopilotToken, setupGitHubToken } from "./lib/token" import { server } from "./server" interface RunServerOptions { @@ -22,7 +24,10 @@ export async function runServer(options: RunServerOptions): Promise { await logger.initialize(options.logFile) - await initializeApp() + await ensurePaths() + await setupGitHubToken() + await setupCopilotToken() + await cacheModels() const serverUrl = `http://localhost:${options.port}` consola.box(`Server started at ${serverUrl}`) From 3ff32210e93544131e06b73027162cbc30471deb Mon Sep 17 00:00:00 2001 From: Erick Christian Date: Tue, 11 Mar 2025 08:40:19 +0700 Subject: [PATCH 10/30] fix: Remove JSON.stringify from user login consola output --- src/lib/token.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/token.ts b/src/lib/token.ts index e4d4f1f7..f7dd2ca3 100644 --- a/src/lib/token.ts +++ b/src/lib/token.ts @@ -59,5 +59,5 @@ export async function setupGitHubToken(): Promise { async function logUser() { const user = await getGitHubUser() - consola.info(`Logged in as ${JSON.stringify(user.login)}\n`) + consola.info(`Logged in as ${user.login}`) } From cb2b18cd6885a97c46d30e79213464c9a6d1d0b4 Mon Sep 17 00:00:00 2001 From: Erick Christian Date: Tue, 11 Mar 2025 08:47:26 +0700 Subject: [PATCH 11/30] Refactor: Use native fetch instead of ofetch for GitHub API calls --- src/lib/constants.ts | 2 + src/lib/token.ts | 2 +- src/services/api-instance.ts | 20 +------ .../copilot/get-token/copilot-token.ts | 32 +++++++++-- src/services/github/get-token/service.ts | 14 ----- src/services/github/get-user.ts | 23 ++++++++ src/services/github/get-user/service.ts | 56 ------------------- 7 files changed, 53 insertions(+), 96 deletions(-) delete mode 100644 src/services/github/get-token/service.ts create mode 100644 src/services/github/get-user.ts delete mode 100644 src/services/github/get-user/service.ts diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 3b443045..54a4d8a5 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -29,6 +29,8 @@ export const GITHUB_WEB_API_CONFIG = { baseURL: "https://github.com", } as const +export const GITHUB_API_BASE_URL = "https://api.github.com" + export const GITHUB_BASE_URL = "https://github.com" export const GITHUB_CLIENT_ID = "01ab8ac9400c4e429b23" export const GITHUB_APP_SCOPES = [ diff --git a/src/lib/token.ts b/src/lib/token.ts index f7dd2ca3..ec05939d 100644 --- a/src/lib/token.ts +++ b/src/lib/token.ts @@ -4,7 +4,7 @@ import fs from "node:fs/promises" import { PATHS } from "~/lib/paths" import { getCopilotToken } from "~/services/copilot/get-token/copilot-token" import { getDeviceCode } from "~/services/github/get-device-code" -import { getGitHubUser } from "~/services/github/get-user/service" +import { getGitHubUser } from "~/services/github/get-user" import { pollAccessToken } from "~/services/github/poll-access-token" import { state } from "./state" diff --git a/src/services/api-instance.ts b/src/services/api-instance.ts index 63bc523f..903b1455 100644 --- a/src/services/api-instance.ts +++ b/src/services/api-instance.ts @@ -1,11 +1,7 @@ import consola from "consola" import { FetchError, ofetch } from "ofetch" -import { - COPILOT_API_CONFIG, - GITHUB_API_CONFIG, - GITHUB_WEB_API_CONFIG, -} from "~/lib/constants" +import { COPILOT_API_CONFIG } from "~/lib/constants" import { modelsCache } from "~/lib/models" import { tokenService } from "~/lib/token" @@ -41,17 +37,3 @@ export const copilot = ofetch.create({ } }, }) - -export const github = ofetch.create({ - baseURL: GITHUB_API_CONFIG.baseURL, - - async onRequest({ options }) { - const token = await tokenService.getGithubToken() - options.headers.set("authorization", `token ${token}`) - }, -}) - -// Only used for device flow auth -export const _github = ofetch.create({ - baseURL: GITHUB_WEB_API_CONFIG.baseURL, -}) diff --git a/src/services/copilot/get-token/copilot-token.ts b/src/services/copilot/get-token/copilot-token.ts index 3bd7f07a..244750d1 100644 --- a/src/services/copilot/get-token/copilot-token.ts +++ b/src/services/copilot/get-token/copilot-token.ts @@ -1,8 +1,28 @@ -import type { GetCopilotTokenResponse } from "./types" +import { GITHUB_API_BASE_URL } from "~/lib/constants" +import { state } from "~/lib/state" -import { github } from "../../api-instance" +export const getCopilotToken = async () => { + const response = await fetch( + `${GITHUB_API_BASE_URL}/copilot_internal/v2/token`, + { + headers: { + authorization: `token ${state.githubToken}`, + }, + }, + ) -export const getCopilotToken = async () => - github("/copilot_internal/v2/token", { - method: "GET", - }) + if (!response.ok) { + throw new Error("Failed to get Copilot token", { + cause: await response.json(), + }) + } + + return (await response.json()) as GetCopilotTokenResponse +} + +// Trimmed for the sake of simplicity +interface GetCopilotTokenResponse { + expires_at: number + refresh_in: number + token: string +} diff --git a/src/services/github/get-token/service.ts b/src/services/github/get-token/service.ts deleted file mode 100644 index 31fd48e8..00000000 --- a/src/services/github/get-token/service.ts +++ /dev/null @@ -1,14 +0,0 @@ -import consola from "consola" - -import { getDeviceCode } from "../get-device-code" -import { pollAccessToken } from "../poll-access-token" - -export async function getGitHubToken() { - const response = await getDeviceCode() - - consola.info( - `Please enter the code "${response.user_code}" in ${response.verification_uri}`, - ) - - return await pollAccessToken(response) -} diff --git a/src/services/github/get-user.ts b/src/services/github/get-user.ts new file mode 100644 index 00000000..55210992 --- /dev/null +++ b/src/services/github/get-user.ts @@ -0,0 +1,23 @@ +import { GITHUB_API_BASE_URL } from "~/lib/constants" +import { state } from "~/lib/state" + +export async function getGitHubUser() { + const response = await fetch(`${GITHUB_API_BASE_URL}/user`, { + headers: { + authorization: `token ${state.githubToken}`, + }, + }) + + if (!response.ok) { + throw new Error("Failed to get GitHub user", { + cause: await response.json(), + }) + } + + return (await response.json()) as GithubUser +} + +// Trimmed for the sake of simplicity +interface GithubUser { + login: string +} diff --git a/src/services/github/get-user/service.ts b/src/services/github/get-user/service.ts deleted file mode 100644 index cd2c2758..00000000 --- a/src/services/github/get-user/service.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { github } from "~/services/api-instance" - -export async function getGitHubUser() { - return github("/user", { - method: "GET", - }) -} - -interface GithubUser { - login: string - id: number - node_id: string - avatar_url: string - gravatar_id: string - url: string - html_url: string - followers_url: string - following_url: string - gists_url: string - starred_url: string - subscriptions_url: string - organizations_url: string - repos_url: string - events_url: string - received_events_url: string - type: "User" - user_view_type: "private" - site_admin: boolean - name: string - company: string | null - blog: string - location: string - email: null - hireable: null - bio: string - twitter_username: string | null - notification_email: null - public_repos: number - public_gists: number - followers: number - following: number - created_at: string - updated_at: string - private_gists: number - total_private_repos: number - owned_private_repos: number - disk_usage: number - collaborators: number - two_factor_authentication: boolean - plan: { - name: "pro" - space: number - collaborators: number - private_repos: number - } -} From 6e5ab6471e12046854b7d7edbc857e15e8130f24 Mon Sep 17 00:00:00 2001 From: Erick Christian Date: Tue, 11 Mar 2025 08:52:02 +0700 Subject: [PATCH 12/30] refactor: Simplify and improve copilot models and github user services --- src/lib/constants.ts | 26 ++------- src/services/copilot/get-models/service.ts | 64 ++++++++++++++++++++-- src/services/github/get-user.ts | 4 +- 3 files changed, 65 insertions(+), 29 deletions(-) diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 54a4d8a5..aa9b206f 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -1,17 +1,3 @@ -// VSCode client ID -const GITHUB_OAUTH_SCOPES = [ - "read:org", - "read:user", - "repo", - "user:email", - "workflow", -].join(" ") - -export const ENV = { - GITHUB_CLIENT_ID: "01ab8ac9400c4e429b23", - GITHUB_OAUTH_SCOPES, -} - export const COPILOT_API_CONFIG = { baseURL: "https://api.individual.githubcopilot.com", headers: { @@ -21,13 +7,11 @@ export const COPILOT_API_CONFIG = { }, } as const -export const GITHUB_API_CONFIG = { - baseURL: "https://api.github.com", -} as const - -export const GITHUB_WEB_API_CONFIG = { - baseURL: "https://github.com", -} as const +export const COPILOT_API_BASE_URL = "https://api.individual.githubcopilot.com" +export const COPILOT_API_HEADERS = { + "copilot-integration-id": "vscode-chat", + "editor-version": "vscode/1.98.0-insider", +} export const GITHUB_API_BASE_URL = "https://api.github.com" diff --git a/src/services/copilot/get-models/service.ts b/src/services/copilot/get-models/service.ts index eded14a3..7ee64f71 100644 --- a/src/services/copilot/get-models/service.ts +++ b/src/services/copilot/get-models/service.ts @@ -1,8 +1,60 @@ -import type { GetModelsResponse } from "./types" +import { COPILOT_API_BASE_URL } from "~/lib/constants" +import { state } from "~/lib/state" -import { copilot } from "../../api-instance" - -export const getModels = () => - copilot("/models", { - method: "GET", +export const getModels = async () => { + const response = await fetch(`${COPILOT_API_BASE_URL}/models`, { + headers: { + authorization: `Bearer ${state.copilotToken}`, + }, }) + + if (!response.ok) { + throw new Error("Failed to get models", { + cause: await response.json(), + }) + } + + return (await response.json()) as ModelsResponse +} + +interface ModelLimits { + max_context_window_tokens?: number + max_output_tokens?: number + max_prompt_tokens?: number + max_inputs?: number +} + +interface ModelSupports { + tool_calls?: boolean + parallel_tool_calls?: boolean + dimensions?: boolean +} + +interface ModelCapabilities { + family: string + limits: ModelLimits + object: string + supports: ModelSupports + tokenizer: string + type: string +} + +interface Model { + capabilities: ModelCapabilities + id: string + model_picker_enabled: boolean + name: string + object: string + preview: boolean + vendor: string + version: string + policy?: { + state: string + terms: string + } +} + +export interface ModelsResponse { + data: Array + object: string +} diff --git a/src/services/github/get-user.ts b/src/services/github/get-user.ts index 55210992..4a01cafd 100644 --- a/src/services/github/get-user.ts +++ b/src/services/github/get-user.ts @@ -14,10 +14,10 @@ export async function getGitHubUser() { }) } - return (await response.json()) as GithubUser + return (await response.json()) as GithubUserResponse } // Trimmed for the sake of simplicity -interface GithubUser { +interface GithubUserResponse { login: string } From 1096cbf2f4c06440f26399ce1c73a68d8d065f07 Mon Sep 17 00:00:00 2001 From: Erick Christian Date: Tue, 11 Mar 2025 09:02:13 +0700 Subject: [PATCH 13/30] refactor: Rename copilot-token service and move models types --- src/lib/models.ts | 2 +- src/lib/state.ts | 4 +- src/lib/token.ts | 2 +- src/routes/models/route.ts | 2 +- .../copilot-token.ts => get-copilot-token.ts} | 0 .../{get-models/service.ts => get-models.ts} | 10 ++--- src/services/copilot/get-models/types.ts | 41 ------------------- src/services/copilot/get-token/types.ts | 31 -------------- 8 files changed, 10 insertions(+), 82 deletions(-) rename src/services/copilot/{get-token/copilot-token.ts => get-copilot-token.ts} (100%) rename src/services/copilot/{get-models/service.ts => get-models.ts} (100%) delete mode 100644 src/services/copilot/get-models/types.ts delete mode 100644 src/services/copilot/get-token/types.ts diff --git a/src/lib/models.ts b/src/lib/models.ts index f6c779d9..97303e73 100644 --- a/src/lib/models.ts +++ b/src/lib/models.ts @@ -2,7 +2,7 @@ import consola from "consola" import type { GetModelsResponse } from "~/services/copilot/get-models/types" -import { getModels } from "~/services/copilot/get-models/service" +import { getModels } from "~/services/copilot/get-models" import { state } from "./state" diff --git a/src/lib/state.ts b/src/lib/state.ts index c0cfae6a..48dfdf08 100644 --- a/src/lib/state.ts +++ b/src/lib/state.ts @@ -1,9 +1,9 @@ -import type { GetModelsResponse } from "~/services/copilot/get-models/types" +import type { ModelsResponse } from "~/services/copilot/get-models" interface State { githubToken?: string copilotToken?: string - models?: GetModelsResponse + models?: ModelsResponse } export const state: State = {} diff --git a/src/lib/token.ts b/src/lib/token.ts index ec05939d..3f47ef3e 100644 --- a/src/lib/token.ts +++ b/src/lib/token.ts @@ -2,7 +2,7 @@ import consola from "consola" import fs from "node:fs/promises" import { PATHS } from "~/lib/paths" -import { getCopilotToken } from "~/services/copilot/get-token/copilot-token" +import { getCopilotToken } from "~/services/copilot/get-copilot-token" import { getDeviceCode } from "~/services/github/get-device-code" import { getGitHubUser } from "~/services/github/get-user" import { pollAccessToken } from "~/services/github/poll-access-token" diff --git a/src/routes/models/route.ts b/src/routes/models/route.ts index 7cd87022..05582fde 100644 --- a/src/routes/models/route.ts +++ b/src/routes/models/route.ts @@ -2,7 +2,7 @@ import consola from "consola" import { Hono } from "hono" import { FetchError } from "ofetch" -import { getModels } from "~/services/copilot/get-models/service" +import { getModels } from "~/services/copilot/get-models" export const modelRoutes = new Hono() diff --git a/src/services/copilot/get-token/copilot-token.ts b/src/services/copilot/get-copilot-token.ts similarity index 100% rename from src/services/copilot/get-token/copilot-token.ts rename to src/services/copilot/get-copilot-token.ts diff --git a/src/services/copilot/get-models/service.ts b/src/services/copilot/get-models.ts similarity index 100% rename from src/services/copilot/get-models/service.ts rename to src/services/copilot/get-models.ts index 7ee64f71..741485a8 100644 --- a/src/services/copilot/get-models/service.ts +++ b/src/services/copilot/get-models.ts @@ -17,6 +17,11 @@ export const getModels = async () => { return (await response.json()) as ModelsResponse } +export interface ModelsResponse { + data: Array + object: string +} + interface ModelLimits { max_context_window_tokens?: number max_output_tokens?: number @@ -53,8 +58,3 @@ interface Model { terms: string } } - -export interface ModelsResponse { - data: Array - object: string -} diff --git a/src/services/copilot/get-models/types.ts b/src/services/copilot/get-models/types.ts deleted file mode 100644 index 7078433f..00000000 --- a/src/services/copilot/get-models/types.ts +++ /dev/null @@ -1,41 +0,0 @@ -interface ModelLimits { - max_context_window_tokens?: number - max_output_tokens?: number - max_prompt_tokens?: number - max_inputs?: number -} - -interface ModelSupports { - tool_calls?: boolean - parallel_tool_calls?: boolean - dimensions?: boolean -} - -interface ModelCapabilities { - family: string - limits: ModelLimits - object: string - supports: ModelSupports - tokenizer: string - type: string -} - -interface Model { - capabilities: ModelCapabilities - id: string - model_picker_enabled: boolean - name: string - object: string - preview: boolean - vendor: string - version: string - policy?: { - state: string - terms: string - } -} - -export interface GetModelsResponse { - data: Array - object: string -} diff --git a/src/services/copilot/get-token/types.ts b/src/services/copilot/get-token/types.ts deleted file mode 100644 index dd456646..00000000 --- a/src/services/copilot/get-token/types.ts +++ /dev/null @@ -1,31 +0,0 @@ -export interface GetCopilotTokenResponse { - annotations_enabled: boolean - chat_enabled: boolean - chat_jetbrains_enabled: boolean - code_quote_enabled: boolean - code_review_enabled: boolean - codesearch: boolean - copilotignore_enabled: boolean - endpoints: { - api: string - "origin-tracker": string - proxy: string - telemetry: string - } - expires_at: number - individual: boolean - limited_user_quotas: null - limited_user_reset_date: null - nes_enabled: boolean - prompt_8k: boolean - public_suggestions: "disabled" - refresh_in: number - sku: "free_educational" - snippy_load_test_enabled: boolean - telemetry: "disabled" - token: string - tracking_id: string - vsc_electron_fetcher_v2: boolean - xcode: boolean - xcode_chat: boolean -} From 6ba6c5bc3577d46633d41d474a1eb9512ff5b982 Mon Sep 17 00:00:00 2001 From: Erick Christian Date: Tue, 11 Mar 2025 10:40:00 +0700 Subject: [PATCH 14/30] refactor: Move API config to api-config.ts and update imports --- src/lib/{constants.ts => api-config.ts} | 8 +++- src/routes/embeddings/route.ts | 4 +- src/services/api-instance.ts | 2 +- .../chat-completions/service-streaming.ts | 2 +- src/services/copilot/embedding/service.ts | 41 ++++++++++++++++--- src/services/copilot/get-copilot-token.ts | 2 +- src/services/copilot/get-models.ts | 2 +- src/services/github/get-device-code.ts | 2 +- src/services/github/get-user.ts | 2 +- src/services/github/poll-access-token.ts | 2 +- 10 files changed, 51 insertions(+), 16 deletions(-) rename src/lib/{constants.ts => api-config.ts} (80%) diff --git a/src/lib/constants.ts b/src/lib/api-config.ts similarity index 80% rename from src/lib/constants.ts rename to src/lib/api-config.ts index aa9b206f..402a29e8 100644 --- a/src/lib/constants.ts +++ b/src/lib/api-config.ts @@ -8,11 +8,15 @@ export const COPILOT_API_CONFIG = { } as const export const COPILOT_API_BASE_URL = "https://api.individual.githubcopilot.com" -export const COPILOT_API_HEADERS = { +const COPILOT_API_SPOOF_HEADERS = { "copilot-integration-id": "vscode-chat", - "editor-version": "vscode/1.98.0-insider", } +export const buildCopilotHeaders = (token: string) => ({ + Authorization: `token ${token}`, + ...COPILOT_API_SPOOF_HEADERS, +}) + export const GITHUB_API_BASE_URL = "https://api.github.com" export const GITHUB_BASE_URL = "https://github.com" diff --git a/src/routes/embeddings/route.ts b/src/routes/embeddings/route.ts index 3e3076bb..9c63e023 100644 --- a/src/routes/embeddings/route.ts +++ b/src/routes/embeddings/route.ts @@ -4,13 +4,13 @@ import { FetchError } from "ofetch" import type { EmbeddingRequest } from "~/services/copilot/embedding/types" -import { embedding } from "~/services/copilot/embedding/service" +import { createEmbeddings } from "~/services/copilot/embedding/service" export const embeddingRoutes = new Hono() embeddingRoutes.post("/", async (c) => { try { - const embeddings = await embedding(await c.req.json()) + const embeddings = await createEmbeddings(await c.req.json()) return c.json(embeddings) } catch (error) { if (error instanceof FetchError) { diff --git a/src/services/api-instance.ts b/src/services/api-instance.ts index 903b1455..72535a14 100644 --- a/src/services/api-instance.ts +++ b/src/services/api-instance.ts @@ -1,7 +1,7 @@ import consola from "consola" import { FetchError, ofetch } from "ofetch" -import { COPILOT_API_CONFIG } from "~/lib/constants" +import { COPILOT_API_CONFIG } from "~/lib/api-config" import { modelsCache } from "~/lib/models" import { tokenService } from "~/lib/token" diff --git a/src/services/copilot/chat-completions/service-streaming.ts b/src/services/copilot/chat-completions/service-streaming.ts index a8b6849b..f18c86a7 100644 --- a/src/services/copilot/chat-completions/service-streaming.ts +++ b/src/services/copilot/chat-completions/service-streaming.ts @@ -1,6 +1,6 @@ import { stream } from "fetch-event-stream" -import { COPILOT_API_CONFIG } from "~/lib/constants" +import { COPILOT_API_CONFIG } from "~/lib/api-config" import { tokenService } from "~/lib/token" import type { ChatCompletionsPayload } from "./types" diff --git a/src/services/copilot/embedding/service.ts b/src/services/copilot/embedding/service.ts index babee8f2..76237464 100644 --- a/src/services/copilot/embedding/service.ts +++ b/src/services/copilot/embedding/service.ts @@ -1,11 +1,42 @@ +import { COPILOT_API_BASE_URL } from "~/lib/api-config" +import { state } from "~/lib/state" + import type { EmbeddingRequest, EmbeddingResponse } from "./types" import { copilot } from "../../api-instance" -export const embedding = (payload: EmbeddingRequest) => - copilot("/embeddings", { - method: "POST", - body: { - ...payload, +export const createEmbeddings = (payload: EmbeddingRequest) => { + const response = await fetch(`${COPILOT_API_BASE_URL}/embeddings`, { + headers: { + authorization: `token ${state.copilotToken}`, }, }) +} + +copilot("/embeddings", { + method: "POST", + body: { + ...payload, + }, +}) + +export interface EmbeddingRequest { + input: string | Array + model: string +} + +export interface Embedding { + object: string + embedding: Array + index: number +} + +export interface EmbeddingResponse { + object: string + data: Array + model: string + usage: { + prompt_tokens: number + total_tokens: number + } +} diff --git a/src/services/copilot/get-copilot-token.ts b/src/services/copilot/get-copilot-token.ts index 244750d1..0ffa2ff5 100644 --- a/src/services/copilot/get-copilot-token.ts +++ b/src/services/copilot/get-copilot-token.ts @@ -1,4 +1,4 @@ -import { GITHUB_API_BASE_URL } from "~/lib/constants" +import { GITHUB_API_BASE_URL } from "~/lib/api-config" import { state } from "~/lib/state" export const getCopilotToken = async () => { diff --git a/src/services/copilot/get-models.ts b/src/services/copilot/get-models.ts index 741485a8..bf6b9c1f 100644 --- a/src/services/copilot/get-models.ts +++ b/src/services/copilot/get-models.ts @@ -1,4 +1,4 @@ -import { COPILOT_API_BASE_URL } from "~/lib/constants" +import { COPILOT_API_BASE_URL } from "~/lib/api-config" import { state } from "~/lib/state" export const getModels = async () => { diff --git a/src/services/github/get-device-code.ts b/src/services/github/get-device-code.ts index 57c969d3..57ec74b2 100644 --- a/src/services/github/get-device-code.ts +++ b/src/services/github/get-device-code.ts @@ -1,4 +1,4 @@ -import { GITHUB_BASE_URL, GITHUB_CLIENT_ID } from "~/lib/constants" +import { GITHUB_BASE_URL, GITHUB_CLIENT_ID } from "~/lib/api-config" export async function getDeviceCode(): Promise { const response = await fetch(`${GITHUB_BASE_URL}/login/device/code`, { diff --git a/src/services/github/get-user.ts b/src/services/github/get-user.ts index 4a01cafd..616aafcd 100644 --- a/src/services/github/get-user.ts +++ b/src/services/github/get-user.ts @@ -1,4 +1,4 @@ -import { GITHUB_API_BASE_URL } from "~/lib/constants" +import { GITHUB_API_BASE_URL } from "~/lib/api-config" import { state } from "~/lib/state" export async function getGitHubUser() { diff --git a/src/services/github/poll-access-token.ts b/src/services/github/poll-access-token.ts index 17d0b32f..52a2f7f7 100644 --- a/src/services/github/poll-access-token.ts +++ b/src/services/github/poll-access-token.ts @@ -1,4 +1,4 @@ -import { GITHUB_BASE_URL, GITHUB_CLIENT_ID } from "~/lib/constants" +import { GITHUB_BASE_URL, GITHUB_CLIENT_ID } from "~/lib/api-config" import { sleep } from "~/lib/sleep" import type { DeviceCodeResponse } from "./get-device-code" From 1d99f2d768e442db8e6b8c74b36614fc7b645ccc Mon Sep 17 00:00:00 2001 From: Erick Christian Date: Tue, 11 Mar 2025 10:43:40 +0700 Subject: [PATCH 15/30] refactor: Move createEmbeddings to its own file and remove old files --- src/routes/embeddings/route.ts | 6 ++-- src/services/copilot/create-embeddings.ts | 43 +++++++++++++++++++++++ src/services/copilot/embedding/service.ts | 42 ---------------------- src/services/copilot/embedding/types.ts | 20 ----------- 4 files changed, 47 insertions(+), 64 deletions(-) create mode 100644 src/services/copilot/create-embeddings.ts delete mode 100644 src/services/copilot/embedding/service.ts delete mode 100644 src/services/copilot/embedding/types.ts diff --git a/src/routes/embeddings/route.ts b/src/routes/embeddings/route.ts index 9c63e023..e9c607ca 100644 --- a/src/routes/embeddings/route.ts +++ b/src/routes/embeddings/route.ts @@ -4,13 +4,15 @@ import { FetchError } from "ofetch" import type { EmbeddingRequest } from "~/services/copilot/embedding/types" -import { createEmbeddings } from "~/services/copilot/embedding/service" +import { createEmbeddings } from "~/services/copilot/create-embeddings" export const embeddingRoutes = new Hono() embeddingRoutes.post("/", async (c) => { try { - const embeddings = await createEmbeddings(await c.req.json()) + const embeddings = await createEmbeddings( + await c.req.json(), + ) return c.json(embeddings) } catch (error) { if (error instanceof FetchError) { diff --git a/src/services/copilot/create-embeddings.ts b/src/services/copilot/create-embeddings.ts new file mode 100644 index 00000000..af4a7864 --- /dev/null +++ b/src/services/copilot/create-embeddings.ts @@ -0,0 +1,43 @@ +import { buildCopilotHeaders, COPILOT_API_BASE_URL } from "~/lib/api-config" +import { state } from "~/lib/state" + +export const createEmbeddings = async (payload: EmbeddingRequest) => { + if (!state.copilotToken) throw new Error("Copilot token not found") + + const response = await fetch(`${COPILOT_API_BASE_URL}/embeddings`, { + method: "POST", + headers: { + ...buildCopilotHeaders(state.copilotToken), + }, + body: JSON.stringify(payload), + }) + + if (!response.ok) { + throw new Error("Failed to create embeddings", { + cause: await response.json(), + }) + } + + return (await response.json()) as EmbeddingResponse +} + +export interface EmbeddingRequest { + input: string | Array + model: string +} + +export interface Embedding { + object: string + embedding: Array + index: number +} + +export interface EmbeddingResponse { + object: string + data: Array + model: string + usage: { + prompt_tokens: number + total_tokens: number + } +} diff --git a/src/services/copilot/embedding/service.ts b/src/services/copilot/embedding/service.ts deleted file mode 100644 index 76237464..00000000 --- a/src/services/copilot/embedding/service.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { COPILOT_API_BASE_URL } from "~/lib/api-config" -import { state } from "~/lib/state" - -import type { EmbeddingRequest, EmbeddingResponse } from "./types" - -import { copilot } from "../../api-instance" - -export const createEmbeddings = (payload: EmbeddingRequest) => { - const response = await fetch(`${COPILOT_API_BASE_URL}/embeddings`, { - headers: { - authorization: `token ${state.copilotToken}`, - }, - }) -} - -copilot("/embeddings", { - method: "POST", - body: { - ...payload, - }, -}) - -export interface EmbeddingRequest { - input: string | Array - model: string -} - -export interface Embedding { - object: string - embedding: Array - index: number -} - -export interface EmbeddingResponse { - object: string - data: Array - model: string - usage: { - prompt_tokens: number - total_tokens: number - } -} diff --git a/src/services/copilot/embedding/types.ts b/src/services/copilot/embedding/types.ts deleted file mode 100644 index cf54610f..00000000 --- a/src/services/copilot/embedding/types.ts +++ /dev/null @@ -1,20 +0,0 @@ -export interface EmbeddingRequest { - input: string | Array - model: string -} - -export interface Embedding { - object: string - embedding: Array - index: number -} - -export interface EmbeddingResponse { - object: string - data: Array - model: string - usage: { - prompt_tokens: number - total_tokens: number - } -} From 43b3d292678a90415541240cb9ec6c54744ae4fa Mon Sep 17 00:00:00 2001 From: Erick Christian Date: Tue, 11 Mar 2025 10:48:07 +0700 Subject: [PATCH 16/30] refactor: Use fetch instead of copilot instance for chat completions --- src/routes/chat-completions/handler.ts | 4 +-- .../copilot/chat-completions/service.ts | 34 +++++++++++++++---- src/services/copilot/create-embeddings.ts | 4 +-- 3 files changed, 31 insertions(+), 11 deletions(-) diff --git a/src/routes/chat-completions/handler.ts b/src/routes/chat-completions/handler.ts index 57f447eb..00f869a4 100644 --- a/src/routes/chat-completions/handler.ts +++ b/src/routes/chat-completions/handler.ts @@ -9,7 +9,7 @@ import type { ChatCompletionChunk } from "~/services/copilot/chat-completions/ty import { isNullish } from "~/lib/is-nullish" import { logger } from "~/lib/logger" import { modelsCache } from "~/lib/models" -import { chatCompletions } from "~/services/copilot/chat-completions/service" +import { createChatCompletions } from "~/services/copilot/chat-completions/service" import { chatCompletionsStream } from "~/services/copilot/chat-completions/service-streaming" function createCondensedStreamingResponse( @@ -85,7 +85,7 @@ function handleStreaming(c: Context, payload: ChatCompletionsPayload) { } async function handleNonStreaming(c: Context, payload: ChatCompletionsPayload) { - const response = await chatCompletions(payload) + const response = await createChatCompletions(payload) // Get response headers if any const responseHeaders = {} // Empty placeholder for response headers diff --git a/src/services/copilot/chat-completions/service.ts b/src/services/copilot/chat-completions/service.ts index d407b68a..db192562 100644 --- a/src/services/copilot/chat-completions/service.ts +++ b/src/services/copilot/chat-completions/service.ts @@ -1,12 +1,34 @@ +import { buildCopilotHeaders, COPILOT_API_BASE_URL } from "~/lib/api-config" +import { state } from "~/lib/state" import { copilot } from "~/services/api-instance" import type { ChatCompletionResponse, ChatCompletionsPayload } from "./types" -export const chatCompletions = (payload: ChatCompletionsPayload) => - copilot("/chat/completions", { +export const createChatCompletions = async ( + payload: ChatCompletionsPayload, +) => { + if (!state.copilotToken) throw new Error("Copilot token not found") + + const response = await fetch(`${COPILOT_API_BASE_URL}/chat/completions`, { method: "POST", - body: { - ...payload, - stream: false, - }, + headers: buildCopilotHeaders(state.copilotToken), + body: JSON.stringify(payload), }) + + if (!response.ok) { + throw new Error("Failed to create chat completions", { + cause: await response.json(), + }) + } + + if (payload.stream) { + } +} + +copilot("/chat/completions", { + method: "POST", + body: { + ...payload, + stream: false, + }, +}) diff --git a/src/services/copilot/create-embeddings.ts b/src/services/copilot/create-embeddings.ts index af4a7864..01380855 100644 --- a/src/services/copilot/create-embeddings.ts +++ b/src/services/copilot/create-embeddings.ts @@ -6,9 +6,7 @@ export const createEmbeddings = async (payload: EmbeddingRequest) => { const response = await fetch(`${COPILOT_API_BASE_URL}/embeddings`, { method: "POST", - headers: { - ...buildCopilotHeaders(state.copilotToken), - }, + headers: buildCopilotHeaders(state.copilotToken), body: JSON.stringify(payload), }) From 0e98c052f990db57f794312aea676d444be210d9 Mon Sep 17 00:00:00 2001 From: Erick Christian Date: Tue, 11 Mar 2025 11:28:25 +0700 Subject: [PATCH 17/30] refactor: Refactor API headers and add chat completion types --- src/lib/api-config.ts | 18 ++++--- src/lib/state.ts | 4 +- src/lib/token.ts | 2 +- src/routes/chat-completions/types.ts | 53 +++++++++++++++++++ .../copilot/chat-completions/service.ts | 18 +++---- src/services/copilot/create-embeddings.ts | 4 +- src/services/get-vscode-version.ts | 19 +++++++ .../{copilot => github}/get-copilot-token.ts | 6 +-- 8 files changed, 99 insertions(+), 25 deletions(-) create mode 100644 src/routes/chat-completions/types.ts create mode 100644 src/services/get-vscode-version.ts rename src/services/{copilot => github}/get-copilot-token.ts (79%) diff --git a/src/lib/api-config.ts b/src/lib/api-config.ts index 402a29e8..0375ab72 100644 --- a/src/lib/api-config.ts +++ b/src/lib/api-config.ts @@ -1,3 +1,5 @@ +import type { State } from "./state" + export const COPILOT_API_CONFIG = { baseURL: "https://api.individual.githubcopilot.com", headers: { @@ -8,16 +10,20 @@ export const COPILOT_API_CONFIG = { } as const export const COPILOT_API_BASE_URL = "https://api.individual.githubcopilot.com" -const COPILOT_API_SPOOF_HEADERS = { +export const copilotHeaders = (state: State) => ({ + Authorization: `token ${state.copilotToken}`, "copilot-integration-id": "vscode-chat", -} - -export const buildCopilotHeaders = (token: string) => ({ - Authorization: `token ${token}`, - ...COPILOT_API_SPOOF_HEADERS, }) export const GITHUB_API_BASE_URL = "https://api.github.com" +export const githubHeaders = (state: State) => ({ + authorization: `token ${state.githubToken}`, + "editor-version": `vscode/${state.vsCodeVersion}`, + "editor-plugin-version": "copilot-chat/0.24.1", + "user-agent": "GitHubCopilotChat/0.24.1", + "x-github-api-version": "2024-12-15", + "x-vscode-user-agent-library-version": "electron-fetch", +}) export const GITHUB_BASE_URL = "https://github.com" export const GITHUB_CLIENT_ID = "01ab8ac9400c4e429b23" diff --git a/src/lib/state.ts b/src/lib/state.ts index 48dfdf08..aa718e9b 100644 --- a/src/lib/state.ts +++ b/src/lib/state.ts @@ -1,9 +1,11 @@ import type { ModelsResponse } from "~/services/copilot/get-models" -interface State { +export interface State { githubToken?: string copilotToken?: string + models?: ModelsResponse + vsCodeVersion?: string } export const state: State = {} diff --git a/src/lib/token.ts b/src/lib/token.ts index 3f47ef3e..c86186ea 100644 --- a/src/lib/token.ts +++ b/src/lib/token.ts @@ -2,7 +2,7 @@ import consola from "consola" import fs from "node:fs/promises" import { PATHS } from "~/lib/paths" -import { getCopilotToken } from "~/services/copilot/get-copilot-token" +import { getCopilotToken } from "~/services/github/get-copilot-token" import { getDeviceCode } from "~/services/github/get-device-code" import { getGitHubUser } from "~/services/github/get-user" import { pollAccessToken } from "~/services/github/poll-access-token" diff --git a/src/routes/chat-completions/types.ts b/src/routes/chat-completions/types.ts new file mode 100644 index 00000000..4ffefb61 --- /dev/null +++ b/src/routes/chat-completions/types.ts @@ -0,0 +1,53 @@ +// https://platform.openai.com/docs/api-reference + +export interface Message { + role: "user" | "assistant" | "system" + content: string +} + +// Streaming types + +export interface ExpectedCompletionChunk { + choices: [Choice] + created: number + object: "chat.completion.chunk" + id: string + model: string +} + +interface Delta { + content?: string + role?: string +} + +interface Choice { + index: number + delta: Delta + finish_reason: "stop" | null + logprobs: null +} + +// Non-streaming types + +export interface ExpectedCompletion { + id: string + object: string + created: number + model: string + choices: [ChoiceNonStreaming] +} + +interface ChoiceNonStreaming { + index: number + message: Message + logprobs: null + finish_reason: "stop" +} + +// Payload types + +export interface ExpectedChatCompletionPayload { + model: string + messages: Array + stream: boolean +} diff --git a/src/services/copilot/chat-completions/service.ts b/src/services/copilot/chat-completions/service.ts index db192562..4855cbf2 100644 --- a/src/services/copilot/chat-completions/service.ts +++ b/src/services/copilot/chat-completions/service.ts @@ -1,6 +1,7 @@ -import { buildCopilotHeaders, COPILOT_API_BASE_URL } from "~/lib/api-config" +import { events } from "fetch-event-stream" + +import { copilotHeaders, COPILOT_API_BASE_URL } from "~/lib/api-config" import { state } from "~/lib/state" -import { copilot } from "~/services/api-instance" import type { ChatCompletionResponse, ChatCompletionsPayload } from "./types" @@ -11,7 +12,7 @@ export const createChatCompletions = async ( const response = await fetch(`${COPILOT_API_BASE_URL}/chat/completions`, { method: "POST", - headers: buildCopilotHeaders(state.copilotToken), + headers: copilotHeaders(state), body: JSON.stringify(payload), }) @@ -22,13 +23,8 @@ export const createChatCompletions = async ( } if (payload.stream) { + return events(response) } -} -copilot("/chat/completions", { - method: "POST", - body: { - ...payload, - stream: false, - }, -}) + return (await response.json()) as ChatCompletionResponse +} diff --git a/src/services/copilot/create-embeddings.ts b/src/services/copilot/create-embeddings.ts index 01380855..810f7c39 100644 --- a/src/services/copilot/create-embeddings.ts +++ b/src/services/copilot/create-embeddings.ts @@ -1,4 +1,4 @@ -import { buildCopilotHeaders, COPILOT_API_BASE_URL } from "~/lib/api-config" +import { copilotHeaders, COPILOT_API_BASE_URL } from "~/lib/api-config" import { state } from "~/lib/state" export const createEmbeddings = async (payload: EmbeddingRequest) => { @@ -6,7 +6,7 @@ export const createEmbeddings = async (payload: EmbeddingRequest) => { const response = await fetch(`${COPILOT_API_BASE_URL}/embeddings`, { method: "POST", - headers: buildCopilotHeaders(state.copilotToken), + headers: copilotHeaders(state), body: JSON.stringify(payload), }) diff --git a/src/services/get-vscode-version.ts b/src/services/get-vscode-version.ts new file mode 100644 index 00000000..ce330838 --- /dev/null +++ b/src/services/get-vscode-version.ts @@ -0,0 +1,19 @@ +const FALLBACK = "1.98.1" + +export async function getVSCodeVersion() { + const response = await fetch( + "https://aur.archlinux.org/cgit/aur.git/plain/PKGBUILD?h=visual-studio-code-bin", + ) + + const pkgbuild = await response.text() + const pkgverRegex = /pkgver=([0-9.]+)/ + const match = pkgbuild.match(pkgverRegex) + + if (match) { + return match[1] + } + + return FALLBACK +} + +await getVSCodeVersion() diff --git a/src/services/copilot/get-copilot-token.ts b/src/services/github/get-copilot-token.ts similarity index 79% rename from src/services/copilot/get-copilot-token.ts rename to src/services/github/get-copilot-token.ts index 0ffa2ff5..9f840fe9 100644 --- a/src/services/copilot/get-copilot-token.ts +++ b/src/services/github/get-copilot-token.ts @@ -1,13 +1,11 @@ -import { GITHUB_API_BASE_URL } from "~/lib/api-config" +import { GITHUB_API_BASE_URL, githubHeaders } from "~/lib/api-config" import { state } from "~/lib/state" export const getCopilotToken = async () => { const response = await fetch( `${GITHUB_API_BASE_URL}/copilot_internal/v2/token`, { - headers: { - authorization: `token ${state.githubToken}`, - }, + headers: githubHeaders(state), }, ) From adcbfca1ce968853614613ff7321c99cac246ff6 Mon Sep 17 00:00:00 2001 From: Erick Christian Date: Tue, 11 Mar 2025 11:31:23 +0700 Subject: [PATCH 18/30] fix: Use Bearer token for copilot authorization and remove logger. --- src/lib/api-config.ts | 2 +- src/lib/logger.ts | 100 ------------------------------------------ src/main.ts | 9 ---- 3 files changed, 1 insertion(+), 110 deletions(-) delete mode 100644 src/lib/logger.ts diff --git a/src/lib/api-config.ts b/src/lib/api-config.ts index 0375ab72..aa3096b0 100644 --- a/src/lib/api-config.ts +++ b/src/lib/api-config.ts @@ -11,7 +11,7 @@ export const COPILOT_API_CONFIG = { export const COPILOT_API_BASE_URL = "https://api.individual.githubcopilot.com" export const copilotHeaders = (state: State) => ({ - Authorization: `token ${state.copilotToken}`, + Authorization: `Bearer ${state.copilotToken}`, "copilot-integration-id": "vscode-chat", }) diff --git a/src/lib/logger.ts b/src/lib/logger.ts deleted file mode 100644 index a6eaf8ba..00000000 --- a/src/lib/logger.ts +++ /dev/null @@ -1,100 +0,0 @@ -import consola from "consola" -import fs from "node:fs/promises" -import path from "pathe" - -export interface LoggerOptions { - enabled: boolean - filePath?: string -} - -export const logger = { - options: { - enabled: false, - filePath: undefined, - } as LoggerOptions, - - async initialize(filePath?: string): Promise { - if (!filePath) { - this.options.enabled = false - return - } - - try { - // Ensure the directory exists - await fs.mkdir(path.dirname(filePath), { recursive: true }) - - // Initialize the log file with a header - const timestamp = new Date().toISOString() - await fs.writeFile( - filePath, - `# API Request/Response Log\n# Started: ${timestamp}\n\n`, - { flag: "w" }, - ) - - this.options.enabled = true - this.options.filePath = filePath - consola.info(`Logging enabled to: ${filePath}`) - } catch (error) { - consola.error(`Failed to initialize log file`, error) - this.options.enabled = false - } - }, - - async logRequest( - endpoint: string, - method: string, - payload: unknown, - headers?: Record, - ): Promise { - if (!this.options.enabled || !this.options.filePath) return - - const timestamp = new Date().toISOString() - const logEntry = [ - `## Request - ${timestamp}`, - `Endpoint: ${endpoint}`, - `Method: ${method}`, - headers ? - `Headers:\n\`\`\`json\n${JSON.stringify(headers, null, 2)}\n\`\`\`` - : "", - `Payload:`, - `\`\`\`json`, - JSON.stringify(payload, null, 2), - `\`\`\``, - `\n`, - ].join("\n") - - try { - await fs.appendFile(this.options.filePath, logEntry) - } catch (error) { - consola.error(`Failed to write to log file`, error) - } - }, - - async logResponse( - endpoint: string, - response: unknown, - headers?: Record, - ): Promise { - if (!this.options.enabled || !this.options.filePath) return - - const timestamp = new Date().toISOString() - const logEntry = [ - `## Response - ${timestamp}`, - `Endpoint: ${endpoint}`, - headers ? - `Headers:\n\`\`\`json\n${JSON.stringify(headers, null, 2)}\n\`\`\`` - : "", - `Response:`, - `\`\`\`json`, - JSON.stringify(response, null, 2), - `\`\`\``, - `\n`, - ].join("\n") - - try { - await fs.appendFile(this.options.filePath, logEntry) - } catch (error) { - consola.error(`Failed to write to log file`, error) - } - }, -} diff --git a/src/main.ts b/src/main.ts index 498a019a..f5a754ff 100644 --- a/src/main.ts +++ b/src/main.ts @@ -4,7 +4,6 @@ import { defineCommand, runMain } from "citty" import consola from "consola" import { serve, type ServerHandler } from "srvx" -import { logger } from "./lib/logger" import { cacheModels } from "./lib/models" import { ensurePaths } from "./lib/paths" import { setupCopilotToken, setupGitHubToken } from "./lib/token" @@ -13,7 +12,6 @@ import { server } from "./server" interface RunServerOptions { port: number verbose: boolean - logFile?: string } export async function runServer(options: RunServerOptions): Promise { @@ -22,8 +20,6 @@ export async function runServer(options: RunServerOptions): Promise { consola.info("Verbose logging enabled") } - await logger.initialize(options.logFile) - await ensurePaths() await setupGitHubToken() await setupCopilotToken() @@ -52,10 +48,6 @@ const main = defineCommand({ default: false, description: "Enable verbose logging", }, - "log-file": { - type: "string", - description: "File to log request/response details", - }, }, run({ args }) { const port = Number.parseInt(args.port, 10) @@ -63,7 +55,6 @@ const main = defineCommand({ return runServer({ port, verbose: args.verbose, - logFile: args["log-file"], }) }, }) From e488fc521cfe7af29b3e98c0899649943100b1b8 Mon Sep 17 00:00:00 2001 From: Erick Christian Date: Tue, 11 Mar 2025 11:37:39 +0700 Subject: [PATCH 19/30] feat: migrate to flat config and add prettier plugin for package.json --- bun.lock | 14 +++++++++++++- eslint.config.js | 3 --- eslint.config.ts | 7 +++++++ package.json | 2 ++ src/lib/port.ts | 19 ------------------- tsconfig.json | 9 ++++----- 6 files changed, 26 insertions(+), 28 deletions(-) delete mode 100644 eslint.config.js create mode 100644 eslint.config.ts delete mode 100644 src/lib/port.ts diff --git a/bun.lock b/bun.lock index 62e44bbc..1fde83fa 100644 --- a/bun.lock +++ b/bun.lock @@ -19,8 +19,10 @@ "@types/bun": "^1.2.4", "bumpp": "^10.0.3", "eslint": "^9.22.0", + "jiti": "^2.4.2", "knip": "^5.45.0", "lint-staged": "^15.4.3", + "prettier-plugin-packagejson": "^2.5.10", "simple-git-hooks": "^2.11.1", "tsup": "^8.4.0", "typescript": "^5.8.2", @@ -806,6 +808,8 @@ "prettier-linter-helpers": ["prettier-linter-helpers@1.0.0", "", { "dependencies": { "fast-diff": "^1.1.2" } }, "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w=="], + "prettier-plugin-packagejson": ["prettier-plugin-packagejson@2.5.10", "", { "dependencies": { "sort-package-json": "2.15.1", "synckit": "0.9.2" }, "peerDependencies": { "prettier": ">= 1.16.0" }, "optionalPeers": ["prettier"] }, "sha512-LUxATI5YsImIVSaaLJlJ3aE6wTD+nvots18U3GuQMJpUyClChaZlQrqx3dBnbhF20OnKWZyx8EgyZypQtBDtgQ=="], + "pretty-ms": ["pretty-ms@9.2.0", "", { "dependencies": { "parse-ms": "^4.0.0" } }, "sha512-4yf0QO/sllf/1zbZWYnvWw3NxCQwLXKzIj0G849LSufP15BXKM0rbD2Z3wVnkMfjdn/CB0Dpp444gYAACdsplg=="], "prompts": ["prompts@2.4.2", "", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="], @@ -888,7 +892,7 @@ "sort-object-keys": ["sort-object-keys@1.1.3", "", {}, "sha512-855pvK+VkU7PaKYPc+Jjnmt4EzejQHyhhF33q31qG8x7maDzkeFhAAThdCYay11CISO+qAMwjOBP+fPZe0IPyg=="], - "sort-package-json": ["sort-package-json@3.0.0", "", { "dependencies": { "detect-indent": "^7.0.1", "detect-newline": "^4.0.1", "get-stdin": "^9.0.0", "git-hooks-list": "^3.0.0", "is-plain-obj": "^4.1.0", "semver": "^7.7.1", "sort-object-keys": "^1.1.3", "tinyglobby": "^0.2.12" }, "bin": { "sort-package-json": "cli.js" } }, "sha512-vfZWx4DnFNB8R9Vg4Dnx21s20auNzWH15ZaCBfADAiyrCwemRmhWstTgvLjMek1DW3+MHcNaqkp86giCF24rMA=="], + "sort-package-json": ["sort-package-json@2.15.1", "", { "dependencies": { "detect-indent": "^7.0.1", "detect-newline": "^4.0.0", "get-stdin": "^9.0.0", "git-hooks-list": "^3.0.0", "is-plain-obj": "^4.1.0", "semver": "^7.6.0", "sort-object-keys": "^1.1.3", "tinyglobby": "^0.2.9" }, "bin": { "sort-package-json": "cli.js" } }, "sha512-9x9+o8krTT2saA9liI4BljNjwAbvUnWf11Wq+i/iZt8nl2UGYnf3TH5uBydE7VALmP7AGwlfszuEeL8BDyb0YA=="], "source-map": ["source-map@0.8.0-beta.0", "", { "dependencies": { "whatwg-url": "^7.0.0" } }, "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA=="], @@ -1084,6 +1088,8 @@ "cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + "eslint-plugin-package-json/sort-package-json": ["sort-package-json@3.0.0", "", { "dependencies": { "detect-indent": "^7.0.1", "detect-newline": "^4.0.1", "get-stdin": "^9.0.0", "git-hooks-list": "^3.0.0", "is-plain-obj": "^4.1.0", "semver": "^7.7.1", "sort-object-keys": "^1.1.3", "tinyglobby": "^0.2.12" }, "bin": { "sort-package-json": "cli.js" } }, "sha512-vfZWx4DnFNB8R9Vg4Dnx21s20auNzWH15ZaCBfADAiyrCwemRmhWstTgvLjMek1DW3+MHcNaqkp86giCF24rMA=="], + "eslint-plugin-perfectionist/@typescript-eslint/types": ["@typescript-eslint/types@8.24.1", "", {}, "sha512-9kqJ+2DkUXiuhoiYIUvIYjGcwle8pcPpdlfkemGvTObzgmYfJ5d0Qm6jwb4NBXP9W1I5tss0VIAnWFumz3mC5A=="], "eslint-plugin-perfectionist/@typescript-eslint/utils": ["@typescript-eslint/utils@8.24.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@typescript-eslint/scope-manager": "8.24.1", "@typescript-eslint/types": "8.24.1", "@typescript-eslint/typescript-estree": "8.24.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.8.0" } }, "sha512-OOcg3PMMQx9EXspId5iktsI3eMaXVwlhC8BvNnX6B5w9a4dVgpkQZuU8Hy67TolKcl+iFWq0XX+jbDGN4xWxjQ=="], @@ -1188,6 +1194,12 @@ "cliui/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + "eslint-plugin-package-json/sort-package-json/detect-indent": ["detect-indent@7.0.1", "", {}, "sha512-Mc7QhQ8s+cLrnUfU/Ji94vG/r8M26m8f++vyres4ZoojaRDpZ1eSIh/EpzLNwlWuvzSZ3UbDFspjFvTDXe6e/g=="], + + "eslint-plugin-package-json/sort-package-json/detect-newline": ["detect-newline@4.0.1", "", {}, "sha512-qE3Veg1YXzGHQhlA6jzebZN2qVf6NX+A7m7qlhCGG30dJixrAQhYOsJjsnBjJkCSmuOPpCk30145fr8FV0bzog=="], + + "eslint-plugin-package-json/sort-package-json/tinyglobby": ["tinyglobby@0.2.12", "", { "dependencies": { "fdir": "^6.4.3", "picomatch": "^4.0.2" } }, "sha512-qkf4trmKSIiMTs/E63cxH+ojC2unam7rJ0WrauAzpT3ECNTxGRMlaXxVbfxMUC/w0LaYk6jQ4y/nGR9uBO3tww=="], + "eslint-plugin-perfectionist/@typescript-eslint/utils/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.24.1", "", { "dependencies": { "@typescript-eslint/types": "8.24.1", "@typescript-eslint/visitor-keys": "8.24.1" } }, "sha512-OdQr6BNBzwRjNEXMQyaGyZzgg7wzjYKfX2ZBV3E04hUCBDv3GQCHiz9RpqdUIiVrMgJGkXm3tcEh4vFSHreS2Q=="], "eslint-plugin-perfectionist/@typescript-eslint/utils/@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.24.1", "", { "dependencies": { "@typescript-eslint/types": "8.24.1", "@typescript-eslint/visitor-keys": "8.24.1", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.0.1" }, "peerDependencies": { "typescript": ">=4.8.4 <5.8.0" } }, "sha512-UPyy4MJ/0RE648DSKQe9g0VDSehPINiejjA6ElqnFaFIhI6ZEiZAkUI0D5MCk0bQcTf/LVqZStvQ6K4lPn/BRg=="], diff --git a/eslint.config.js b/eslint.config.js deleted file mode 100644 index f3e96aeb..00000000 --- a/eslint.config.js +++ /dev/null @@ -1,3 +0,0 @@ -import config from "@echristian/eslint-config" - -export default config() diff --git a/eslint.config.ts b/eslint.config.ts new file mode 100644 index 00000000..c9f79bea --- /dev/null +++ b/eslint.config.ts @@ -0,0 +1,7 @@ +import config from "@echristian/eslint-config" + +export default config({ + prettier: { + plugins: ["prettier-plugin-packagejson"], + }, +}) diff --git a/package.json b/package.json index e38c2626..2e80a10f 100644 --- a/package.json +++ b/package.json @@ -54,8 +54,10 @@ "@types/bun": "^1.2.4", "bumpp": "^10.0.3", "eslint": "^9.22.0", + "jiti": "^2.4.2", "knip": "^5.45.0", "lint-staged": "^15.4.3", + "prettier-plugin-packagejson": "^2.5.10", "simple-git-hooks": "^2.11.1", "tsup": "^8.4.0", "typescript": "^5.8.2" diff --git a/src/lib/port.ts b/src/lib/port.ts deleted file mode 100644 index 8a5262ec..00000000 --- a/src/lib/port.ts +++ /dev/null @@ -1,19 +0,0 @@ -import consola from "consola" -import { getPort } from "get-port-please" - -export async function initializePort(requestedPort?: number): Promise { - const port = await getPort({ - name: "copilot-api", - port: requestedPort, - portRange: [4142, 4200], - random: false, - }) - - if (port !== requestedPort) { - consola.warn( - `Default port ${requestedPort} is already in use. Using port ${port} instead.`, - ) - } - - return port -} diff --git a/tsconfig.json b/tsconfig.json index c453b492..7c910323 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,12 +1,12 @@ { "compilerOptions": { // Enable latest features - "lib": ["ESNext", "DOM"], "target": "ESNext", + "lib": ["ESNext"], "module": "ESNext", - "moduleDetection": "force", - "jsx": "react-jsx", - "allowJs": true, + "skipLibCheck": true, + + // Bundler mode "moduleResolution": "bundler", @@ -16,7 +16,6 @@ // Best practices "strict": true, - "skipLibCheck": true, "noFallthroughCasesInSwitch": true, // Some stricter flags (disabled by default) From e2af351957bcb7c9e91fc6a218d265bcc4379a8a Mon Sep 17 00:00:00 2001 From: Erick Christian Date: Tue, 11 Mar 2025 11:39:06 +0700 Subject: [PATCH 20/30] build: Configure TypeScript for better module handling and strictness --- .vscode/settings.json | 3 ++- tsconfig.json | 18 ++++++------------ 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 124d331e..145f6642 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,5 +4,6 @@ "editor.codeActionsOnSave": { "source.fixAll.eslint": "explicit", "source.organizeImports": "never" - } + }, + "typescript.tsdk": "node_modules/typescript/lib" } diff --git a/tsconfig.json b/tsconfig.json index 7c910323..bfff5e6b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,27 +1,21 @@ { "compilerOptions": { - // Enable latest features "target": "ESNext", "lib": ["ESNext"], "module": "ESNext", "skipLibCheck": true, - - - // Bundler mode - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, + "moduleResolution": "Bundler", + "moduleDetection": "force", + "erasableSyntaxOnly": true, "verbatimModuleSyntax": true, "noEmit": true, - // Best practices "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, "noFallthroughCasesInSwitch": true, - - // Some stricter flags (disabled by default) - "noUnusedLocals": false, - "noUnusedParameters": false, - "noPropertyAccessFromIndexSignature": false, + "noUncheckedSideEffectImports": true, "baseUrl": ".", "paths": { From 67d0eecf13d025e0e64bb3b74e2942751dffacdd Mon Sep 17 00:00:00 2001 From: Erick Christian Date: Tue, 11 Mar 2025 11:48:54 +0700 Subject: [PATCH 21/30] chore: Update enhanced-resolve and knip config --- bun.lock | 4 +--- knip.config.ts | 3 +-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/bun.lock b/bun.lock index 1fde83fa..3cbd4f82 100644 --- a/bun.lock +++ b/bun.lock @@ -384,7 +384,7 @@ "emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], - "enhanced-resolve": ["enhanced-resolve@5.18.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-0/r0MySGYG8YqlayBZ6MuCfECmHFdJ5qyPh8s8wa5Hnm6SaFLSK1VYCbj+NKp090Nm1caZhD+QTnmxO7esYGyQ=="], + "enhanced-resolve": ["enhanced-resolve@5.18.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg=="], "environment": ["environment@1.1.0", "", {}, "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q=="], @@ -1114,8 +1114,6 @@ "jsonc-eslint-parser/espree": ["espree@9.6.1", "", { "dependencies": { "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^3.4.1" } }, "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ=="], - "knip/zod": ["zod@3.24.1", "", {}, "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A=="], - "lint-staged/chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="], "log-update/slice-ansi": ["slice-ansi@7.1.0", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg=="], diff --git a/knip.config.ts b/knip.config.ts index f9ac749d..c4829825 100644 --- a/knip.config.ts +++ b/knip.config.ts @@ -1,6 +1,5 @@ import type { KnipConfig } from "knip" export default { - entry: ["./src/main.ts"], - ignore: ["scripts/*.ts"], + entry: ["./src/main.ts", "./eslint.config.ts"], } satisfies KnipConfig From b5f8ce634c686e7a64238f675c529900ee5b6a53 Mon Sep 17 00:00:00 2001 From: Erick Christian Date: Tue, 11 Mar 2025 11:51:25 +0700 Subject: [PATCH 22/30] chore: remove unused dependencies and knip config --- bun.lock | 8 +------- knip.config.ts | 5 ----- package.json | 2 -- src/lib/token.ts | 5 ++--- 4 files changed, 3 insertions(+), 17 deletions(-) delete mode 100644 knip.config.ts diff --git a/bun.lock b/bun.lock index 3cbd4f82..46569333 100644 --- a/bun.lock +++ b/bun.lock @@ -7,10 +7,8 @@ "citty": "^0.1.6", "consola": "^3.4.0", "fetch-event-stream": "^0.1.5", - "get-port-please": "^3.1.2", "hono": "^4.7.4", "ofetch": "^1.4.1", - "pathe": "^2.0.3", "srvx": "^0.2.5", "zod": "^3.24.2", }, @@ -512,8 +510,6 @@ "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], - "get-port-please": ["get-port-please@3.1.2", "", {}, "sha512-Gxc29eLs1fbn6LQ4jSU4vXjlwyZhF5HsGuMAa7gqBP4Rw4yxxltyDUuF5MBclFzDTXO+ACchGQoeela4DSfzdQ=="], - "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], "get-stdin": ["get-stdin@9.0.0", "", {}, "sha512-dVKBjfWisLAicarI2Sf+JuBE/DghV4UzNAVe9yhEJuzeREd3JhOTE9cUaJTeSa77fsbQUK3pcOpJfM59+VKZaA=="], @@ -780,7 +776,7 @@ "path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], - "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + "pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="], "perfect-debounce": ["perfect-debounce@1.0.0", "", {}, "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA=="], @@ -1076,8 +1072,6 @@ "aggregate-error/indent-string": ["indent-string@4.0.0", "", {}, "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg=="], - "c12/pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="], - "citty/consola": ["consola@3.3.3", "", {}, "sha512-Qil5KwghMzlqd51UXM0b6fyaGHtOC22scxrwrz4A2882LyUMwQjnvaedN1HAeXzphspQ6CpHkzMAWxBTUruDLg=="], "clean-regexp/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="], diff --git a/knip.config.ts b/knip.config.ts deleted file mode 100644 index c4829825..00000000 --- a/knip.config.ts +++ /dev/null @@ -1,5 +0,0 @@ -import type { KnipConfig } from "knip" - -export default { - entry: ["./src/main.ts", "./eslint.config.ts"], -} satisfies KnipConfig diff --git a/package.json b/package.json index 2e80a10f..76348641 100644 --- a/package.json +++ b/package.json @@ -42,10 +42,8 @@ "citty": "^0.1.6", "consola": "^3.4.0", "fetch-event-stream": "^0.1.5", - "get-port-please": "^3.1.2", "hono": "^4.7.4", "ofetch": "^1.4.1", - "pathe": "^2.0.3", "srvx": "^0.2.5", "zod": "^3.24.2" }, diff --git a/src/lib/token.ts b/src/lib/token.ts index c86186ea..18292dce 100644 --- a/src/lib/token.ts +++ b/src/lib/token.ts @@ -9,10 +9,9 @@ import { pollAccessToken } from "~/services/github/poll-access-token" import { state } from "./state" -export const readGithubToken = () => - fs.readFile(PATHS.GITHUB_TOKEN_PATH, "utf8") +const readGithubToken = () => fs.readFile(PATHS.GITHUB_TOKEN_PATH, "utf8") -export const writeGithubToken = (token: string) => +const writeGithubToken = (token: string) => fs.writeFile(PATHS.GITHUB_TOKEN_PATH, token) export const setupCopilotToken = async () => { From 1654e647b06afe4673815e005a32e729a596ee70 Mon Sep 17 00:00:00 2001 From: Erick Christian Date: Tue, 11 Mar 2025 13:28:28 +0700 Subject: [PATCH 23/30] refactor: Simplify error handling and remove unused logging features --- src/routes/chat-completions/handler.ts | 83 +----------- src/routes/chat-completions/route.ts | 171 ++++--------------------- src/services/api-instance.ts | 39 ------ 3 files changed, 24 insertions(+), 269 deletions(-) delete mode 100644 src/services/api-instance.ts diff --git a/src/routes/chat-completions/handler.ts b/src/routes/chat-completions/handler.ts index 00f869a4..8b266731 100644 --- a/src/routes/chat-completions/handler.ts +++ b/src/routes/chat-completions/handler.ts @@ -1,107 +1,34 @@ import type { Context } from "hono" -import consola from "consola" import { streamSSE, type SSEMessage } from "hono/streaming" import type { ChatCompletionsPayload } from "~/services/copilot/chat-completions/types" -import type { ChatCompletionChunk } from "~/services/copilot/chat-completions/types-streaming" import { isNullish } from "~/lib/is-nullish" -import { logger } from "~/lib/logger" -import { modelsCache } from "~/lib/models" +import { state } from "~/lib/state" import { createChatCompletions } from "~/services/copilot/chat-completions/service" import { chatCompletionsStream } from "~/services/copilot/chat-completions/service-streaming" -function createCondensedStreamingResponse( - finalChunk: ChatCompletionChunk, - collectedContent: string, -) { - return { - id: finalChunk.id, - model: finalChunk.model, - created: finalChunk.created, - object: "chat.completion", - system_fingerprint: finalChunk.system_fingerprint, - usage: finalChunk.usage, - choices: [ - { - index: 0, - finish_reason: finalChunk.choices[0].finish_reason, - message: { - role: "assistant", - content: collectedContent, - }, - content_filter_results: finalChunk.choices[0].content_filter_results, - }, - ], - } -} - function handleStreaming(c: Context, payload: ChatCompletionsPayload) { return streamSSE(c, async (stream) => { const response = await chatCompletionsStream(payload) - // For collecting the complete streaming response - let collectedContent = "" - let finalChunk: ChatCompletionChunk | null = null - for await (const chunk of response) { await stream.writeSSE(chunk as SSEMessage) - - if (!logger.options.enabled) continue - - // Check if chunk data is "DONE" or not a valid JSON string - if (!chunk.data || chunk.data === "[DONE]") { - continue // Skip processing this chunk for logging - } - - try { - const data = JSON.parse(chunk.data) as ChatCompletionChunk - - // Keep track of the latest chunk for metadata - finalChunk = data - - // Accumulate content from each delta - if (typeof data.choices[0].delta.content === "string") { - collectedContent += data.choices[0].delta.content - } - } catch (error) { - // Handle JSON parsing errors gracefully - consola.error(`Error parsing SSE chunk data`, error) - // Continue processing other chunks - } - } - - // After streaming completes, log the condensed response - if (finalChunk) { - const condensedResponse = createCondensedStreamingResponse( - finalChunk, - collectedContent, - ) - - await logger.logResponse("/chat/completions", condensedResponse, {}) } }) } async function handleNonStreaming(c: Context, payload: ChatCompletionsPayload) { const response = await createChatCompletions(payload) - - // Get response headers if any - const responseHeaders = {} // Empty placeholder for response headers - - // Log the non-streaming response with headers - await logger.logResponse("/chat/completions", response, responseHeaders) - return c.json(response) } export async function handleCompletion(c: Context) { - const models = modelsCache.getModels() let payload = await c.req.json() if (isNullish(payload.max_tokens)) { - const selectedModel = models?.data.find( + const selectedModel = state.models?.data.find( (model) => model.id === payload.model, ) @@ -111,12 +38,6 @@ export async function handleCompletion(c: Context) { } } - // Convert request headers to a regular object from Headers - const requestHeaders = c.req.header() - - // Log the request at the beginning for both streaming and non-streaming cases - await logger.logRequest("/chat/completions", "POST", payload, requestHeaders) - if (payload.stream) { return handleStreaming(c, payload) } diff --git a/src/routes/chat-completions/route.ts b/src/routes/chat-completions/route.ts index 64aca155..4696da4e 100644 --- a/src/routes/chat-completions/route.ts +++ b/src/routes/chat-completions/route.ts @@ -1,11 +1,6 @@ -import type { BlankEnv, BlankInput } from "hono/types" -import type { ContentfulStatusCode } from "hono/utils/http-status" - import consola from "consola" -import { Hono, type Context } from "hono" -import { FetchError } from "ofetch" +import { Hono } from "hono" -import { logger } from "../../lib/logger" import { handleCompletion } from "./handler" export const completionRoutes = new Hono() @@ -15,149 +10,27 @@ completionRoutes.post("/", async (c) => { return await handleCompletion(c) } catch (error) { consola.error("Error occurred:", error) - return handleError(c, error) - } -}) - -// Handle different error types with specific handlers -async function handleError( - c: Context, - error: unknown, -) { - if (error instanceof FetchError) { - return handleFetchError(c, error) - } - - if (error instanceof Response) { - return await handleResponseError(c, error) - } - - if (error instanceof Error) { - return handleGenericError(c, error) - } - - // Fallback for unknown error types - void logger.logResponse("/v1/chat/completions", { - error: { - message: "An unknown error occurred", - type: "unknown_error", - }, - }) - - return c.json( - { - error: { - message: "An unknown error occurred", - type: "unknown_error", - }, - }, - 500, - ) -} - -function handleFetchError( - c: Context, - error: FetchError, -) { - const status = error.response?.status ?? 500 - const responseData = error.response?._data as unknown - const headers: Record = {} - - // Forward all headers from the error response - for (const [key, value] of error.response?.headers.entries()) { - c.header(key, value) - headers[key] = value - } - - // Log the error response - void logger.logResponse( - "/v1/chat/completions", - { - error: { - message: error.message, - type: "fetch_error", - data: responseData, - status, - }, - }, - headers, - ) - return c.json( - { - error: { - message: error.message, - type: "fetch_error", - data: responseData, + if (error instanceof Response) { + return c.json( + { + error: { + message: error.message, + type: "error", + }, + }, + 500, + ) + } + + return c.json( + { + error: { + message: error.message, + type: "error", + }, }, - }, - status as ContentfulStatusCode, - ) -} - -async function handleResponseError( - c: Context, - error: Response, -) { - const errorText = await error.text() - consola.error( - `Request failed: ${error.status} ${error.statusText}: ${errorText}`, - ) - - const headers: Record = {} - - // Forward all headers from the error response - for (const [key, value] of error.headers.entries()) { - c.header(key, value) - headers[key] = value + 500, + ) } - - // Log the error response - void logger.logResponse( - "/v1/chat/completions", - { - error: { - message: error.statusText || "Request failed", - type: "response_error", - status: error.status, - details: errorText, - }, - }, - headers, - ) - - return c.json( - { - error: { - message: error.statusText || "Request failed", - type: "response_error", - status: error.status, - details: errorText, - }, - }, - error.status as ContentfulStatusCode, - ) -} - -function handleGenericError( - c: Context, - error: Error, -) { - // Log the error response - void logger.logResponse("/v1/chat/completions", { - error: { - message: error.message, - type: "error", - }, - }) - - return c.json( - { - error: { - message: error.message, - type: "error", - }, - }, - 500, - ) -} +}) diff --git a/src/services/api-instance.ts b/src/services/api-instance.ts deleted file mode 100644 index 72535a14..00000000 --- a/src/services/api-instance.ts +++ /dev/null @@ -1,39 +0,0 @@ -import consola from "consola" -import { FetchError, ofetch } from "ofetch" - -import { COPILOT_API_CONFIG } from "~/lib/api-config" -import { modelsCache } from "~/lib/models" -import { tokenService } from "~/lib/token" - -export const copilot = ofetch.create({ - baseURL: COPILOT_API_CONFIG.baseURL, - headers: COPILOT_API_CONFIG.headers, - - onRequest({ options }) { - options.headers.set( - "authorization", - `Bearer ${tokenService.getCopilotToken()}`, - ) - }, - - onRequestError({ error, options }) { - if (error instanceof FetchError) { - consola.error(`Request failed: ${options.body} \n ${error}`) - } - }, - - onResponse({ response }) { - if (response.url.endsWith("/models") && response._data) { - modelsCache.setModels(response._data) - } - }, - - onResponseError({ error, response, options }) { - if (error instanceof FetchError) { - consola.error( - // eslint-disable-next-line @typescript-eslint/no-base-to-string, @typescript-eslint/restrict-template-expressions - `Request failed: ${options.body} \n ${error} \n with response: ${JSON.stringify(response)}`, - ) - } - }, -}) From c589db2694144a5490fd0f8d49372fdb38d1f53a Mon Sep 17 00:00:00 2001 From: Erick Christian Date: Tue, 11 Mar 2025 14:32:07 +0700 Subject: [PATCH 24/30] refactor: Refactor error handling and copilot API calls --- bun.lock | 8 +-- package.json | 4 +- src/lib/api-config.ts | 7 +++ src/lib/forward-error.ts | 32 +++++++++++ src/lib/http-error.ts | 8 +++ src/routes/chat-completions/handler.ts | 39 +++++++------- src/routes/chat-completions/route.ts | 27 ++-------- src/routes/chat-completions/types.ts | 53 ------------------- src/routes/embeddings/route.ts | 23 ++++---- .../chat-completions/service-streaming.ts | 21 -------- .../service.ts => create-chat-completions.ts} | 13 ++--- src/services/copilot/create-embeddings.ts | 7 +-- src/services/copilot/get-models.ts | 7 +-- src/services/github/get-copilot-token.ts | 7 +-- src/services/github/get-device-code.ts | 7 +-- src/services/github/get-user.ts | 7 +-- 16 files changed, 97 insertions(+), 173 deletions(-) create mode 100644 src/lib/forward-error.ts create mode 100644 src/lib/http-error.ts delete mode 100644 src/routes/chat-completions/types.ts delete mode 100644 src/services/copilot/chat-completions/service-streaming.ts rename src/services/copilot/{chat-completions/service.ts => create-chat-completions.ts} (71%) diff --git a/bun.lock b/bun.lock index 46569333..76c1477c 100644 --- a/bun.lock +++ b/bun.lock @@ -8,9 +8,7 @@ "consola": "^3.4.0", "fetch-event-stream": "^0.1.5", "hono": "^4.7.4", - "ofetch": "^1.4.1", "srvx": "^0.2.5", - "zod": "^3.24.2", }, "devDependencies": { "@echristian/eslint-config": "^0.0.32", @@ -720,7 +718,7 @@ "natural-orderby": ["natural-orderby@5.0.0", "", {}, "sha512-kKHJhxwpR/Okycz4HhQKKlhWe4ASEfPgkSWNmKFHd7+ezuQlxkA5cM3+XkBPvm1gmHen3w53qsYAv+8GwRrBlg=="], - "node-fetch-native": ["node-fetch-native@1.6.4", "", {}, "sha512-IhOigYzAKHd244OC0JIMIUrjzctirCmPkaIfhDeGcEETWof5zKYUW7e7MYvChGWh/4CJeXEgsRyGzuF334rOOQ=="], + "node-fetch-native": ["node-fetch-native@1.6.6", "", {}, "sha512-8Mc2HhqPdlIfedsuZoc3yioPuzp6b+L5jRCRY1QzuWZh2EGJVQrGppC6V6cF0bLdbW0+O2YpqCA25aF/1lvipQ=="], "node-releases": ["node-releases@2.0.19", "", {}, "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw=="], @@ -742,8 +740,6 @@ "object.values": ["object.values@1.2.1", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA=="], - "ofetch": ["ofetch@1.4.1", "", { "dependencies": { "destr": "^2.0.3", "node-fetch-native": "^1.6.4", "ufo": "^1.5.4" } }, "sha512-QZj2DfGplQAr2oj9KzceK9Hwz6Whxazmn85yYeVuS3u9XTMOGMRx0kO95MQ+vLsj/S/NwBDMMLU5hpxvI6Tklw=="], - "ohash": ["ohash@1.1.4", "", {}, "sha512-FlDryZAahJmEF3VR3w1KogSEdWX3WhA5GPakFx4J81kEAiHyLMpdLLElS8n8dfNadMgAne/MywcvmogzscVt4g=="], "onetime": ["onetime@6.0.0", "", { "dependencies": { "mimic-fn": "^4.0.0" } }, "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ=="], @@ -1096,8 +1092,6 @@ "fs-minipass/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], - "giget/node-fetch-native": ["node-fetch-native@1.6.6", "", {}, "sha512-8Mc2HhqPdlIfedsuZoc3yioPuzp6b+L5jRCRY1QzuWZh2EGJVQrGppC6V6cF0bLdbW0+O2YpqCA25aF/1lvipQ=="], - "giget/pathe": ["pathe@2.0.2", "", {}, "sha512-15Ztpk+nov8DR524R4BF7uEuzESgzUEAV4Ah7CUMNGXdE5ELuvxElxGXndBl32vMSsWa1jpNf22Z+Er3sKwq+w=="], "glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], diff --git a/package.json b/package.json index 76348641..95181b2e 100644 --- a/package.json +++ b/package.json @@ -43,9 +43,7 @@ "consola": "^3.4.0", "fetch-event-stream": "^0.1.5", "hono": "^4.7.4", - "ofetch": "^1.4.1", - "srvx": "^0.2.5", - "zod": "^3.24.2" + "srvx": "^0.2.5" }, "devDependencies": { "@echristian/eslint-config": "^0.0.32", diff --git a/src/lib/api-config.ts b/src/lib/api-config.ts index aa3096b0..ea1c9c96 100644 --- a/src/lib/api-config.ts +++ b/src/lib/api-config.ts @@ -12,7 +12,14 @@ export const COPILOT_API_CONFIG = { export const COPILOT_API_BASE_URL = "https://api.individual.githubcopilot.com" export const copilotHeaders = (state: State) => ({ Authorization: `Bearer ${state.copilotToken}`, + "content-type": "application/json", "copilot-integration-id": "vscode-chat", + "editor-version": `vscode/${state.vsCodeVersion}`, + "editor-plugin-version": "copilot-chat/0.24.1", + "openai-intent": "conversation-panel", + "x-github-api-version": "2024-12-15", + "x-request-id": globalThis.crypto.randomUUID(), + "x-vscode-user-agent-library-version": "electron-fetch", }) export const GITHUB_API_BASE_URL = "https://api.github.com" diff --git a/src/lib/forward-error.ts b/src/lib/forward-error.ts new file mode 100644 index 00000000..bf9b87c3 --- /dev/null +++ b/src/lib/forward-error.ts @@ -0,0 +1,32 @@ +import type { Context } from "hono" +import type { ContentfulStatusCode } from "hono/utils/http-status" + +import consola from "consola" + +import { HTTPError } from "./http-error" + +export async function forwardError(c: Context, error: unknown) { + consola.error("Error occurred:", error) + + if (error instanceof HTTPError) { + return c.json( + { + error: { + message: await error.response.text(), + type: "error", + }, + }, + error.response.status as ContentfulStatusCode, + ) + } + + return c.json( + { + error: { + message: (error as Error).message, + type: "error", + }, + }, + 500, + ) +} diff --git a/src/lib/http-error.ts b/src/lib/http-error.ts new file mode 100644 index 00000000..352d3c62 --- /dev/null +++ b/src/lib/http-error.ts @@ -0,0 +1,8 @@ +export class HTTPError extends Error { + response: Response + + constructor(message: string, response: Response) { + super(message) + this.response = response + } +} diff --git a/src/routes/chat-completions/handler.ts b/src/routes/chat-completions/handler.ts index 8b266731..d0ecb4ae 100644 --- a/src/routes/chat-completions/handler.ts +++ b/src/routes/chat-completions/handler.ts @@ -2,27 +2,14 @@ import type { Context } from "hono" import { streamSSE, type SSEMessage } from "hono/streaming" -import type { ChatCompletionsPayload } from "~/services/copilot/chat-completions/types" +import type { + ChatCompletionResponse, + ChatCompletionsPayload, +} from "~/services/copilot/chat-completions/types" import { isNullish } from "~/lib/is-nullish" import { state } from "~/lib/state" -import { createChatCompletions } from "~/services/copilot/chat-completions/service" -import { chatCompletionsStream } from "~/services/copilot/chat-completions/service-streaming" - -function handleStreaming(c: Context, payload: ChatCompletionsPayload) { - return streamSSE(c, async (stream) => { - const response = await chatCompletionsStream(payload) - - for await (const chunk of response) { - await stream.writeSSE(chunk as SSEMessage) - } - }) -} - -async function handleNonStreaming(c: Context, payload: ChatCompletionsPayload) { - const response = await createChatCompletions(payload) - return c.json(response) -} +import { createChatCompletions } from "~/services/copilot/create-chat-completions" export async function handleCompletion(c: Context) { let payload = await c.req.json() @@ -38,9 +25,19 @@ export async function handleCompletion(c: Context) { } } - if (payload.stream) { - return handleStreaming(c, payload) + const response = await createChatCompletions(payload) + + if (isNonStreaming(response)) { + return c.json(response) } - return handleNonStreaming(c, payload) + return streamSSE(c, async (stream) => { + for await (const chunk of response) { + await stream.writeSSE(chunk as SSEMessage) + } + }) } + +const isNonStreaming = ( + response: Awaited>, +): response is ChatCompletionResponse => Object.hasOwn(response, "choices") diff --git a/src/routes/chat-completions/route.ts b/src/routes/chat-completions/route.ts index 4696da4e..67d8b479 100644 --- a/src/routes/chat-completions/route.ts +++ b/src/routes/chat-completions/route.ts @@ -1,6 +1,7 @@ -import consola from "consola" import { Hono } from "hono" +import { forwardError } from "~/lib/forward-error" + import { handleCompletion } from "./handler" export const completionRoutes = new Hono() @@ -9,28 +10,6 @@ completionRoutes.post("/", async (c) => { try { return await handleCompletion(c) } catch (error) { - consola.error("Error occurred:", error) - - if (error instanceof Response) { - return c.json( - { - error: { - message: error.message, - type: "error", - }, - }, - 500, - ) - } - - return c.json( - { - error: { - message: error.message, - type: "error", - }, - }, - 500, - ) + return forwardError(c, error) } }) diff --git a/src/routes/chat-completions/types.ts b/src/routes/chat-completions/types.ts deleted file mode 100644 index 4ffefb61..00000000 --- a/src/routes/chat-completions/types.ts +++ /dev/null @@ -1,53 +0,0 @@ -// https://platform.openai.com/docs/api-reference - -export interface Message { - role: "user" | "assistant" | "system" - content: string -} - -// Streaming types - -export interface ExpectedCompletionChunk { - choices: [Choice] - created: number - object: "chat.completion.chunk" - id: string - model: string -} - -interface Delta { - content?: string - role?: string -} - -interface Choice { - index: number - delta: Delta - finish_reason: "stop" | null - logprobs: null -} - -// Non-streaming types - -export interface ExpectedCompletion { - id: string - object: string - created: number - model: string - choices: [ChoiceNonStreaming] -} - -interface ChoiceNonStreaming { - index: number - message: Message - logprobs: null - finish_reason: "stop" -} - -// Payload types - -export interface ExpectedChatCompletionPayload { - model: string - messages: Array - stream: boolean -} diff --git a/src/routes/embeddings/route.ts b/src/routes/embeddings/route.ts index e9c607ca..c57df15a 100644 --- a/src/routes/embeddings/route.ts +++ b/src/routes/embeddings/route.ts @@ -1,23 +1,20 @@ -import consola from "consola" import { Hono } from "hono" -import { FetchError } from "ofetch" -import type { EmbeddingRequest } from "~/services/copilot/embedding/types" - -import { createEmbeddings } from "~/services/copilot/create-embeddings" +import { forwardError } from "~/lib/forward-error" +import { + createEmbeddings, + type EmbeddingRequest, +} from "~/services/copilot/create-embeddings" export const embeddingRoutes = new Hono() embeddingRoutes.post("/", async (c) => { try { - const embeddings = await createEmbeddings( - await c.req.json(), - ) - return c.json(embeddings) + const paylod = await c.req.json() + const response = await createEmbeddings(paylod) + + return c.json(response) } catch (error) { - if (error instanceof FetchError) { - consola.error(`Request failed: ${error.message}`, error.response?._data) - } - throw error + return forwardError(c, error) } }) diff --git a/src/services/copilot/chat-completions/service-streaming.ts b/src/services/copilot/chat-completions/service-streaming.ts deleted file mode 100644 index f18c86a7..00000000 --- a/src/services/copilot/chat-completions/service-streaming.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { stream } from "fetch-event-stream" - -import { COPILOT_API_CONFIG } from "~/lib/api-config" -import { tokenService } from "~/lib/token" - -import type { ChatCompletionsPayload } from "./types" - -export const chatCompletionsStream = async ( - payload: ChatCompletionsPayload, -) => { - const copilotToken = tokenService.getCopilotToken() - - return stream(`${COPILOT_API_CONFIG.baseURL}/chat/completions`, { - method: "POST", - headers: { - ...COPILOT_API_CONFIG.headers, - authorization: `Bearer ${copilotToken}`, - }, - body: JSON.stringify(payload), - }) -} diff --git a/src/services/copilot/chat-completions/service.ts b/src/services/copilot/create-chat-completions.ts similarity index 71% rename from src/services/copilot/chat-completions/service.ts rename to src/services/copilot/create-chat-completions.ts index 4855cbf2..bab69030 100644 --- a/src/services/copilot/chat-completions/service.ts +++ b/src/services/copilot/create-chat-completions.ts @@ -1,9 +1,13 @@ import { events } from "fetch-event-stream" import { copilotHeaders, COPILOT_API_BASE_URL } from "~/lib/api-config" +import { HTTPError } from "~/lib/http-error" import { state } from "~/lib/state" -import type { ChatCompletionResponse, ChatCompletionsPayload } from "./types" +import type { + ChatCompletionResponse, + ChatCompletionsPayload, +} from "./chat-completions/types" export const createChatCompletions = async ( payload: ChatCompletionsPayload, @@ -16,11 +20,8 @@ export const createChatCompletions = async ( body: JSON.stringify(payload), }) - if (!response.ok) { - throw new Error("Failed to create chat completions", { - cause: await response.json(), - }) - } + if (!response.ok) + throw new HTTPError("Failed to create chat completions", response) if (payload.stream) { return events(response) diff --git a/src/services/copilot/create-embeddings.ts b/src/services/copilot/create-embeddings.ts index 810f7c39..075aed91 100644 --- a/src/services/copilot/create-embeddings.ts +++ b/src/services/copilot/create-embeddings.ts @@ -1,4 +1,5 @@ import { copilotHeaders, COPILOT_API_BASE_URL } from "~/lib/api-config" +import { HTTPError } from "~/lib/http-error" import { state } from "~/lib/state" export const createEmbeddings = async (payload: EmbeddingRequest) => { @@ -10,11 +11,7 @@ export const createEmbeddings = async (payload: EmbeddingRequest) => { body: JSON.stringify(payload), }) - if (!response.ok) { - throw new Error("Failed to create embeddings", { - cause: await response.json(), - }) - } + if (!response.ok) throw new HTTPError("Failed to create embeddings", response) return (await response.json()) as EmbeddingResponse } diff --git a/src/services/copilot/get-models.ts b/src/services/copilot/get-models.ts index bf6b9c1f..5362e83d 100644 --- a/src/services/copilot/get-models.ts +++ b/src/services/copilot/get-models.ts @@ -1,4 +1,5 @@ import { COPILOT_API_BASE_URL } from "~/lib/api-config" +import { HTTPError } from "~/lib/http-error" import { state } from "~/lib/state" export const getModels = async () => { @@ -8,11 +9,7 @@ export const getModels = async () => { }, }) - if (!response.ok) { - throw new Error("Failed to get models", { - cause: await response.json(), - }) - } + if (!response.ok) throw new HTTPError("Failed to get models", response) return (await response.json()) as ModelsResponse } diff --git a/src/services/github/get-copilot-token.ts b/src/services/github/get-copilot-token.ts index 9f840fe9..55701f30 100644 --- a/src/services/github/get-copilot-token.ts +++ b/src/services/github/get-copilot-token.ts @@ -1,4 +1,5 @@ import { GITHUB_API_BASE_URL, githubHeaders } from "~/lib/api-config" +import { HTTPError } from "~/lib/http-error" import { state } from "~/lib/state" export const getCopilotToken = async () => { @@ -9,11 +10,7 @@ export const getCopilotToken = async () => { }, ) - if (!response.ok) { - throw new Error("Failed to get Copilot token", { - cause: await response.json(), - }) - } + if (!response.ok) throw new HTTPError("Failed to get Copilot token", response) return (await response.json()) as GetCopilotTokenResponse } diff --git a/src/services/github/get-device-code.ts b/src/services/github/get-device-code.ts index 57ec74b2..31d743f3 100644 --- a/src/services/github/get-device-code.ts +++ b/src/services/github/get-device-code.ts @@ -1,4 +1,5 @@ import { GITHUB_BASE_URL, GITHUB_CLIENT_ID } from "~/lib/api-config" +import { HTTPError } from "~/lib/http-error" export async function getDeviceCode(): Promise { const response = await fetch(`${GITHUB_BASE_URL}/login/device/code`, { @@ -8,11 +9,7 @@ export async function getDeviceCode(): Promise { }), }) - if (!response.ok) { - throw new Error("Failed to get device code", { - cause: await response.json(), - }) - } + if (!response.ok) throw new HTTPError("Failed to get device code", response) return (await response.json()) as DeviceCodeResponse } diff --git a/src/services/github/get-user.ts b/src/services/github/get-user.ts index 616aafcd..156e91e7 100644 --- a/src/services/github/get-user.ts +++ b/src/services/github/get-user.ts @@ -1,4 +1,5 @@ import { GITHUB_API_BASE_URL } from "~/lib/api-config" +import { HTTPError } from "~/lib/http-error" import { state } from "~/lib/state" export async function getGitHubUser() { @@ -8,11 +9,7 @@ export async function getGitHubUser() { }, }) - if (!response.ok) { - throw new Error("Failed to get GitHub user", { - cause: await response.json(), - }) - } + if (!response.ok) throw new HTTPError("Failed to get GitHub user", response) return (await response.json()) as GithubUserResponse } From 20c50dc6b34567bf5ad38cd021554f16d6116c32 Mon Sep 17 00:00:00 2001 From: Erick Christian Date: Tue, 11 Mar 2025 14:33:48 +0700 Subject: [PATCH 25/30] fix: Handle errors and cache models correctly --- src/lib/forward-error.ts | 3 ++- src/lib/models.ts | 14 -------------- src/routes/chat-completions/route.ts | 2 +- src/routes/embeddings/route.ts | 2 +- src/routes/models/route.ts | 8 ++------ 5 files changed, 6 insertions(+), 23 deletions(-) diff --git a/src/lib/forward-error.ts b/src/lib/forward-error.ts index bf9b87c3..c0a1e02c 100644 --- a/src/lib/forward-error.ts +++ b/src/lib/forward-error.ts @@ -9,10 +9,11 @@ export async function forwardError(c: Context, error: unknown) { consola.error("Error occurred:", error) if (error instanceof HTTPError) { + const errorText = await error.response.text() return c.json( { error: { - message: await error.response.text(), + message: errorText, type: "error", }, }, diff --git a/src/lib/models.ts b/src/lib/models.ts index 97303e73..d6a3516b 100644 --- a/src/lib/models.ts +++ b/src/lib/models.ts @@ -1,23 +1,9 @@ import consola from "consola" -import type { GetModelsResponse } from "~/services/copilot/get-models/types" - import { getModels } from "~/services/copilot/get-models" import { state } from "./state" -export const modelsCache = { - _models: null as GetModelsResponse | null, - - setModels(models: GetModelsResponse) { - this._models = models - }, - - getModels() { - return this._models - }, -} - export async function cacheModels(): Promise { const models = await getModels() state.models = models diff --git a/src/routes/chat-completions/route.ts b/src/routes/chat-completions/route.ts index 67d8b479..c55a3a7b 100644 --- a/src/routes/chat-completions/route.ts +++ b/src/routes/chat-completions/route.ts @@ -10,6 +10,6 @@ completionRoutes.post("/", async (c) => { try { return await handleCompletion(c) } catch (error) { - return forwardError(c, error) + return await forwardError(c, error) } }) diff --git a/src/routes/embeddings/route.ts b/src/routes/embeddings/route.ts index c57df15a..f18c8645 100644 --- a/src/routes/embeddings/route.ts +++ b/src/routes/embeddings/route.ts @@ -15,6 +15,6 @@ embeddingRoutes.post("/", async (c) => { return c.json(response) } catch (error) { - return forwardError(c, error) + return await forwardError(c, error) } }) diff --git a/src/routes/models/route.ts b/src/routes/models/route.ts index 05582fde..8e282a39 100644 --- a/src/routes/models/route.ts +++ b/src/routes/models/route.ts @@ -1,7 +1,6 @@ -import consola from "consola" import { Hono } from "hono" -import { FetchError } from "ofetch" +import { forwardError } from "~/lib/forward-error" import { getModels } from "~/services/copilot/get-models" export const modelRoutes = new Hono() @@ -11,9 +10,6 @@ modelRoutes.get("/", async (c) => { const models = await getModels() return c.json(models) } catch (error) { - if (error instanceof FetchError) { - consola.error(`Request failed: ${error.message}`, error.response?._data) - } - throw error + return await forwardError(c, error) } }) From c9f992ee3b177df3da63a83cad2fa24f2abddfed Mon Sep 17 00:00:00 2001 From: Erick Christian Date: Tue, 11 Mar 2025 14:34:33 +0700 Subject: [PATCH 26/30] chore: Remove test script --- package.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/package.json b/package.json index 95181b2e..f9d4f39b 100644 --- a/package.json +++ b/package.json @@ -29,8 +29,7 @@ "prepack": "bun run build", "prepare": "simple-git-hooks", "release": "bumpp && bun publish --access public", - "start": "NODE_ENV=production bun run ./src/main.ts", - "test": "vitest" + "start": "NODE_ENV=production bun run ./src/main.ts" }, "simple-git-hooks": { "pre-commit": "bunx lint-staged" From b24db815783a0322d357c2c185ed2d3c64d612c2 Mon Sep 17 00:00:00 2001 From: Erick Christian Date: Tue, 11 Mar 2025 14:38:46 +0700 Subject: [PATCH 27/30] feat: Cache VSCode version on startup --- src/lib/api-config.ts | 9 --------- src/lib/vscode-version.ts | 12 ++++++++++++ src/main.ts | 2 ++ 3 files changed, 14 insertions(+), 9 deletions(-) create mode 100644 src/lib/vscode-version.ts diff --git a/src/lib/api-config.ts b/src/lib/api-config.ts index ea1c9c96..641a003e 100644 --- a/src/lib/api-config.ts +++ b/src/lib/api-config.ts @@ -1,14 +1,5 @@ import type { State } from "./state" -export const COPILOT_API_CONFIG = { - baseURL: "https://api.individual.githubcopilot.com", - headers: { - "copilot-integration-id": "vscode-chat", - "copilot-vision-request": "true", - "editor-version": "vscode/1.98.0-insider", - }, -} as const - export const COPILOT_API_BASE_URL = "https://api.individual.githubcopilot.com" export const copilotHeaders = (state: State) => ({ Authorization: `Bearer ${state.copilotToken}`, diff --git a/src/lib/vscode-version.ts b/src/lib/vscode-version.ts new file mode 100644 index 00000000..5b330113 --- /dev/null +++ b/src/lib/vscode-version.ts @@ -0,0 +1,12 @@ +import consola from "consola" + +import { getVSCodeVersion } from "~/services/get-vscode-version" + +import { state } from "./state" + +export const cacheVSCodeVersion = async () => { + const response = await getVSCodeVersion() + state.vsCodeVersion = response + + consola.info(`Using VSCode version: ${response}`) +} diff --git a/src/main.ts b/src/main.ts index f5a754ff..bc7568dd 100644 --- a/src/main.ts +++ b/src/main.ts @@ -7,6 +7,7 @@ import { serve, type ServerHandler } from "srvx" import { cacheModels } from "./lib/models" import { ensurePaths } from "./lib/paths" import { setupCopilotToken, setupGitHubToken } from "./lib/token" +import { cacheVSCodeVersion } from "./lib/vscode-version" import { server } from "./server" interface RunServerOptions { @@ -21,6 +22,7 @@ export async function runServer(options: RunServerOptions): Promise { } await ensurePaths() + await cacheVSCodeVersion() await setupGitHubToken() await setupCopilotToken() await cacheModels() From 2f293abb4215bcc33b582a79cd39e666a7b30796 Mon Sep 17 00:00:00 2001 From: Erick Christian Date: Tue, 11 Mar 2025 14:41:10 +0700 Subject: [PATCH 28/30] fix: request device code with scopes --- src/services/github/get-device-code.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/services/github/get-device-code.ts b/src/services/github/get-device-code.ts index 31d743f3..fc1919f0 100644 --- a/src/services/github/get-device-code.ts +++ b/src/services/github/get-device-code.ts @@ -1,4 +1,8 @@ -import { GITHUB_BASE_URL, GITHUB_CLIENT_ID } from "~/lib/api-config" +import { + GITHUB_APP_SCOPES, + GITHUB_BASE_URL, + GITHUB_CLIENT_ID, +} from "~/lib/api-config" import { HTTPError } from "~/lib/http-error" export async function getDeviceCode(): Promise { @@ -6,6 +10,7 @@ export async function getDeviceCode(): Promise { method: "POST", body: JSON.stringify({ client_id: GITHUB_CLIENT_ID, + scope: GITHUB_APP_SCOPES, }), }) From de13b29e673e4b4aaad24731d3e7d2eb69c3dd41 Mon Sep 17 00:00:00 2001 From: Erick Christian Date: Tue, 11 Mar 2025 15:17:33 +0700 Subject: [PATCH 29/30] refactor: Improve error handling and token management --- README.md | 34 ++++++++++--------- src/lib/api-config.ts | 8 ++++- src/lib/token.ts | 42 +++++++++++++++--------- src/services/copilot/get-models.ts | 6 ++-- src/services/github/get-device-code.ts | 2 ++ src/services/github/get-user.ts | 3 +- src/services/github/poll-access-token.ts | 25 ++++++++++---- 7 files changed, 77 insertions(+), 43 deletions(-) diff --git a/README.md b/README.md index d57fb89a..52290a4c 100644 --- a/README.md +++ b/README.md @@ -40,18 +40,17 @@ npx copilot-api --port 8080 ## Command Line Options -The command accepts several command line options: +The following command line options are available: -| Option | Description | Default | -| ------------- | ------------------------------------ | ------- | -| --port, -p | Port to listen on | 4141 | -| --verbose, -v | Enable verbose logging | false | -| --log-file | File to log request/response details | - | +| Option | Description | Default | +| ------------- | ------------------------ | ------- | +| --port, -p | Port to listen on | 4141 | +| --verbose, -v | Enable verbose logging | false | -Example with options: +Example usage: ```sh -npx copilot-api@latest --port 8080 --verbose --log-file copilot-api.txt +npx copilot-api@latest --port 8080 --verbose ``` ## Running from Source @@ -70,15 +69,18 @@ bun run dev bun run start ``` -## Tips to not hit the rate limit +## Usage Tips -- Use a free model from free provider like Gemini/Mistral/Openrouter for the weak model -- Rarely use architect mode -- Do not enable automatic yes in aider config -- Claude 3.7 thinking mode uses more tokens. Use it sparingly +To avoid rate limiting and optimize your experience: + +- Consider using free models (e.g., Gemini, Mistral, Openrouter) as the `weak-model` +- Use architect mode sparingly +- Disable `yes-always` in your aider configuration +- Be mindful that Claude 3.7 thinking mode consume more tokens ## Roadmap -- Manual approval for every request -- Rate limiting (only allow request every X seconds) -- Token counting +- [ ] Manual request approval system +- [ ] Rate limiting implementation +- [ ] Token usage tracking and monitoring +- [ ] Enhanced error handling and recovery diff --git a/src/lib/api-config.ts b/src/lib/api-config.ts index 641a003e..3a13370c 100644 --- a/src/lib/api-config.ts +++ b/src/lib/api-config.ts @@ -1,9 +1,14 @@ import type { State } from "./state" +export const standardHeaders = () => ({ + "content-type": "application/json", + accept: "application/json", +}) + export const COPILOT_API_BASE_URL = "https://api.individual.githubcopilot.com" export const copilotHeaders = (state: State) => ({ Authorization: `Bearer ${state.copilotToken}`, - "content-type": "application/json", + "content-type": standardHeaders()["content-type"], "copilot-integration-id": "vscode-chat", "editor-version": `vscode/${state.vsCodeVersion}`, "editor-plugin-version": "copilot-chat/0.24.1", @@ -15,6 +20,7 @@ export const copilotHeaders = (state: State) => ({ export const GITHUB_API_BASE_URL = "https://api.github.com" export const githubHeaders = (state: State) => ({ + ...standardHeaders(), authorization: `token ${state.githubToken}`, "editor-version": `vscode/${state.vsCodeVersion}`, "editor-plugin-version": "copilot-chat/0.24.1", diff --git a/src/lib/token.ts b/src/lib/token.ts index 18292dce..7fbad77a 100644 --- a/src/lib/token.ts +++ b/src/lib/token.ts @@ -7,6 +7,7 @@ import { getDeviceCode } from "~/services/github/get-device-code" import { getGitHubUser } from "~/services/github/get-user" import { pollAccessToken } from "~/services/github/poll-access-token" +import { HTTPError } from "./http-error" import { state } from "./state" const readGithubToken = () => fs.readFile(PATHS.GITHUB_TOKEN_PATH, "utf8") @@ -33,27 +34,38 @@ export const setupCopilotToken = async () => { } export async function setupGitHubToken(): Promise { - const githubToken = await readGithubToken() + try { + const githubToken = await readGithubToken() - if (githubToken) { - state.githubToken = githubToken - await logUser() + if (githubToken) { + state.githubToken = githubToken + await logUser() - return - } + return + } + + consola.info("Not logged in, getting new access token") + const response = await getDeviceCode() + consola.debug("Device code response:", response) - consola.info("Not logged in, getting new access token") - const response = await getDeviceCode() + consola.info( + `Please enter the code "${response.user_code}" in ${response.verification_uri}`, + ) - consola.info( - `Please enter the code "${response.user_code}" in ${response.verification_uri}`, - ) + const token = await pollAccessToken(response) + await writeGithubToken(token) + state.githubToken = token - const token = await pollAccessToken(response) - await writeGithubToken(token) - state.githubToken = token + await logUser() + } catch (error) { + if (error instanceof HTTPError) { + consola.error("Failed to get GitHub token:", await error.response.json()) + throw error + } - await logUser() + consola.error("Failed to get GitHub token:", error) + throw error + } } async function logUser() { diff --git a/src/services/copilot/get-models.ts b/src/services/copilot/get-models.ts index 5362e83d..dd940a7f 100644 --- a/src/services/copilot/get-models.ts +++ b/src/services/copilot/get-models.ts @@ -1,12 +1,10 @@ -import { COPILOT_API_BASE_URL } from "~/lib/api-config" +import { COPILOT_API_BASE_URL, copilotHeaders } from "~/lib/api-config" import { HTTPError } from "~/lib/http-error" import { state } from "~/lib/state" export const getModels = async () => { const response = await fetch(`${COPILOT_API_BASE_URL}/models`, { - headers: { - authorization: `Bearer ${state.copilotToken}`, - }, + headers: copilotHeaders(state), }) if (!response.ok) throw new HTTPError("Failed to get models", response) diff --git a/src/services/github/get-device-code.ts b/src/services/github/get-device-code.ts index fc1919f0..1c3bebbb 100644 --- a/src/services/github/get-device-code.ts +++ b/src/services/github/get-device-code.ts @@ -2,12 +2,14 @@ import { GITHUB_APP_SCOPES, GITHUB_BASE_URL, GITHUB_CLIENT_ID, + standardHeaders, } from "~/lib/api-config" import { HTTPError } from "~/lib/http-error" export async function getDeviceCode(): Promise { const response = await fetch(`${GITHUB_BASE_URL}/login/device/code`, { method: "POST", + headers: standardHeaders(), body: JSON.stringify({ client_id: GITHUB_CLIENT_ID, scope: GITHUB_APP_SCOPES, diff --git a/src/services/github/get-user.ts b/src/services/github/get-user.ts index 156e91e7..21590754 100644 --- a/src/services/github/get-user.ts +++ b/src/services/github/get-user.ts @@ -1,4 +1,4 @@ -import { GITHUB_API_BASE_URL } from "~/lib/api-config" +import { GITHUB_API_BASE_URL, standardHeaders } from "~/lib/api-config" import { HTTPError } from "~/lib/http-error" import { state } from "~/lib/state" @@ -6,6 +6,7 @@ export async function getGitHubUser() { const response = await fetch(`${GITHUB_API_BASE_URL}/user`, { headers: { authorization: `token ${state.githubToken}`, + ...standardHeaders(), }, }) diff --git a/src/services/github/poll-access-token.ts b/src/services/github/poll-access-token.ts index 52a2f7f7..938ff70b 100644 --- a/src/services/github/poll-access-token.ts +++ b/src/services/github/poll-access-token.ts @@ -1,4 +1,10 @@ -import { GITHUB_BASE_URL, GITHUB_CLIENT_ID } from "~/lib/api-config" +import consola from "consola" + +import { + GITHUB_BASE_URL, + GITHUB_CLIENT_ID, + standardHeaders, +} from "~/lib/api-config" import { sleep } from "~/lib/sleep" import type { DeviceCodeResponse } from "./get-device-code" @@ -6,11 +12,17 @@ import type { DeviceCodeResponse } from "./get-device-code" export async function pollAccessToken( deviceCode: DeviceCodeResponse, ): Promise { + // Interval is in seconds, we need to multiply by 1000 to get milliseconds + // I'm also adding another second, just to be safe + const sleepDuration = (deviceCode.interval + 1) * 1000 + consola.debug(`Polling access token with interval of ${sleepDuration}ms`) + while (true) { const response = await fetch( `${GITHUB_BASE_URL}/login/oauth/access_token`, { method: "POST", + headers: standardHeaders(), body: JSON.stringify({ client_id: GITHUB_CLIENT_ID, device_code: deviceCode.device_code, @@ -19,16 +31,17 @@ export async function pollAccessToken( }, ) - // Interval is in seconds, we need to multiply by 1000 to get milliseconds - // I'm also adding another second, just to be safe - const sleepDuration = (deviceCode.interval + 1) * 1000 - if (!response.ok) { await sleep(sleepDuration) + consola.error("Failed to poll access token:", await response.text()) + continue } - const { access_token } = (await response.json()) as AccessTokenResponse + const json = await response.json() + consola.debug("Polling access token response:", json) + + const { access_token } = json as AccessTokenResponse if (access_token) { return access_token From bd5eaedf7b4d3601bf85b7ff060942be5e315898 Mon Sep 17 00:00:00 2001 From: Erick Christian Date: Tue, 11 Mar 2025 15:20:20 +0700 Subject: [PATCH 30/30] docs: Update README with minor fixes and roadmap item --- README.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 52290a4c..f2ead0c7 100644 --- a/README.md +++ b/README.md @@ -42,10 +42,10 @@ npx copilot-api --port 8080 The following command line options are available: -| Option | Description | Default | -| ------------- | ------------------------ | ------- | -| --port, -p | Port to listen on | 4141 | -| --verbose, -v | Enable verbose logging | false | +| Option | Description | Default | +| ------------- | ---------------------- | ------- | +| --port, -p | Port to listen on | 4141 | +| --verbose, -v | Enable verbose logging | false | Example usage: @@ -80,6 +80,7 @@ To avoid rate limiting and optimize your experience: ## Roadmap +- [ ] Manual authentication flow - [ ] Manual request approval system - [ ] Rate limiting implementation - [ ] Token usage tracking and monitoring