Skip to content

Commit a153fd9

Browse files
committed
feat: Implement chat completions and caching
1 parent f225582 commit a153fd9

9 files changed

Lines changed: 146 additions & 19 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "copilot-api",
33
"scripts": {
4-
"dev": "bun run ./src/main.ts",
4+
"dev": "bun run --hot ./src/main.ts",
55
"start": "bun run ./src/main.ts"
66
},
77
"dependencies": {

src/lib/cache.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { PATHS } from "./paths"
2+
3+
interface CacheEntry {
4+
value: string
5+
createdAt: number
6+
}
7+
8+
type Cache = Record<string, CacheEntry>
9+
10+
const readCache = async () => {
11+
const content = await Bun.file(PATHS.PATH_CACHE_FILE).text()
12+
return JSON.parse(content) as Cache
13+
}
14+
15+
const writeCache = (cache: Cache) =>
16+
Bun.write(PATHS.PATH_CACHE_FILE, JSON.stringify(cache))
17+
18+
const setCache = async (key: string, value: string) => {
19+
const cache = await readCache()
20+
cache[key] = {
21+
value,
22+
createdAt: Date.now(),
23+
}
24+
25+
return writeCache(cache)
26+
}
27+
28+
const getCache = async (key: string): Promise<CacheEntry | undefined> => {
29+
const cache = await readCache()
30+
return cache[key]
31+
}
32+
33+
export const CACHE = {
34+
get: getCache,
35+
set: setCache,
36+
37+
// Lower level methods
38+
_read: readCache,
39+
_write: writeCache,
40+
}

src/lib/paths.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ import path from "pathe"
33

44
const DIR_CACHE = path.join(os.homedir(), ".cache", "copilot-api")
55

6-
const PATH_TOKEN_CACHE = path.join(DIR_CACHE, "token")
6+
const PATH_CACHE_FILE = path.join(DIR_CACHE, "cache.json")
77

88
export const PATHS = {
99
DIR_CACHE,
10-
PATH_TOKEN_CACHE,
10+
PATH_CACHE_FILE,
1111
}

src/main.ts

Lines changed: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,36 @@
11
import consola from "consola"
2+
import fs from "node:fs"
23

4+
import { CACHE } from "./lib/cache"
5+
import { PATHS } from "./lib/paths"
36
import { TOKENS } from "./lib/tokens"
4-
import { chatCompletions } from "./services/copilot-vscode/chat-completions/service"
7+
import { server } from "./server"
8+
import { getModels } from "./services/copilot-vscode/get-models/service"
59
import { getCopilotToken } from "./services/copilot-vscode/get-token/copilot-token"
610
import { getGitHubToken } from "./services/copilot-vscode/get-token/github-token"
711

8-
const githubToken = await getGitHubToken()
12+
if (!fs.existsSync(PATHS.PATH_CACHE_FILE)) {
13+
fs.mkdirSync(PATHS.DIR_CACHE, { recursive: true })
14+
await CACHE._write({})
15+
}
16+
17+
let githubToken: string
18+
19+
const cachedGithubToken = await CACHE.get("github-token")
20+
21+
const FOUR_HOURS = 4 * 60 * 60 * 1000
22+
23+
// If exists and at most 4 hours old
24+
if (
25+
cachedGithubToken &&
26+
Date.now() - cachedGithubToken.createdAt < FOUR_HOURS
27+
) {
28+
githubToken = cachedGithubToken.value
29+
} else {
30+
githubToken = await getGitHubToken()
31+
await CACHE.set("github-token", githubToken)
32+
}
33+
934
TOKENS.GITHUB_TOKEN = githubToken
1035

1136
const { token: copilotToken, refresh_in } = await getCopilotToken()
@@ -21,17 +46,10 @@ setInterval(async () => {
2146
TOKENS.COPILOT_TOKEN = copilotToken
2247
}, refreshInterval)
2348

24-
const response = await chatCompletions({
25-
messages: [
26-
{
27-
role: "user",
28-
content: "Write a function that returns the sum of two numbers",
29-
},
30-
],
31-
model: "gpt-4o-mini",
32-
stream: false,
33-
})
49+
const models = await getModels()
3450

35-
console.log(response)
51+
consola.info(
52+
`Available models: ${models.data.map((model) => model.id).join("\n")}`,
53+
)
3654

37-
await Bun.write("response.json", JSON.stringify(response, null, 2))
55+
export default server
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { Hono } from "hono"
2+
import { streamSSE } from "hono/streaming"
3+
4+
import type { ChatCompletionsPayload } from "~/services/copilot-vscode/chat-completions/types"
5+
import type { ChatCompletionsChunk } from "~/services/copilot-vscode/chat-completions/types.streaming"
6+
7+
import { chatCompletions } from "~/services/copilot-vscode/chat-completions/service"
8+
9+
export const chatCompletionsRoutes = new Hono()
10+
11+
chatCompletionsRoutes.post("/chat/completions", async (c) => {
12+
const payload = await c.req.json<ChatCompletionsPayload>()
13+
14+
payload.stream = false
15+
16+
await Bun.write("payload.json", JSON.stringify(payload))
17+
18+
const response = await chatCompletions(payload)
19+
await Bun.write("response.json", JSON.stringify(response))
20+
21+
const chunks: Array<ChatCompletionsChunk> = [
22+
{
23+
data: {
24+
choices: [
25+
{
26+
delta: {
27+
content: response.choices[0].message.content,
28+
role: response.choices[0].message.role,
29+
},
30+
finish_reason: response.choices[0].finish_reason,
31+
index: 0,
32+
},
33+
],
34+
created: response.created,
35+
id: response.id,
36+
model: response.model,
37+
},
38+
},
39+
{
40+
data: "[DONE]",
41+
},
42+
]
43+
44+
return streamSSE(c, async (stream) => {
45+
for (const chunk of chunks) {
46+
await stream.writeSSE({
47+
data: JSON.stringify(chunk.data),
48+
})
49+
}
50+
})
51+
})

src/server.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { Hono } from "hono"
2+
import { logger } from "hono/logger"
3+
4+
import { chatCompletionsRoutes } from "./routes/chat-completions/route"
5+
6+
export const server = new Hono()
7+
8+
server.use(logger())
9+
10+
server.get("/", (c) => c.text("Server running"))
11+
server.route("/", chatCompletionsRoutes)

src/services/copilot-vscode/api-instance.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import consola from "consola"
12
import { ofetch } from "ofetch"
23

34
import { TOKENS } from "~/lib/tokens"
@@ -11,6 +12,12 @@ export const copilotVSCode = ofetch.create({
1112
onRequest({ options }) {
1213
options.headers.set("authorization", `Bearer ${TOKENS.COPILOT_TOKEN}`)
1314
},
15+
onRequestError({ error }) {
16+
consola.error("request error", error)
17+
},
18+
onResponseError({ error, response }) {
19+
consola.error("response error", error, response)
20+
},
1421
})
1522

1623
export const github = ofetch.create({

src/services/copilot-vscode/chat-completions/types.streaming.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ interface Usage {
5252
}
5353

5454
interface ChatCompletionResponse {
55-
choices: Array<Choice>
55+
choices: [Choice]
5656
created: number
5757
id: string
5858
model: string

src/services/copilot-vscode/chat-completions/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ interface Usage {
5757
}
5858

5959
export interface ChatCompletionResponse {
60-
choices: Array<Choice>
60+
choices: [Choice]
6161
created: number
6262
id: string
6363
model: string

0 commit comments

Comments
 (0)