Skip to content

Commit a5c14e8

Browse files
committed
feat: [wip] chat completions
1 parent 7de7059 commit a5c14e8

14 files changed

Lines changed: 625 additions & 0 deletions

File tree

docs/api/completions.txt

Lines changed: 213 additions & 0 deletions
Large diffs are not rendered by default.

docs/api/models.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
GET https://api.individual.githubcopilot.com/models HTTP/2.0
2+
authorization:
3+
copilot-integration-id: vscode-chat
4+
5+
{"data":[{"capabilities":{"family":"gpt-3.5-turbo","limits":{"max_context_window_tokens":16384,"max_output_tokens":4096,"max_prompt_tokens":12288},"object":"model_capabilities","supports":{"tool_calls":true},"tokenizer":"cl100k_base","type":"chat"},"id":"gpt-3.5-turbo","model_picker_enabled":false,"name":"GPT 3.5 Turbo","object":"model","preview":false,"vendor":"Azure OpenAI","version":"gpt-3.5-turbo-0613"},{"capabilities":{"family":"gpt-3.5-turbo","limits":{"max_context_window_tokens":16384,"max_output_tokens":4096,"max_prompt_tokens":12288},"object":"model_capabilities","supports":{"tool_calls":true},"tokenizer":"cl100k_base","type":"chat"},"id":"gpt-3.5-turbo-0613","model_picker_enabled":false,"name":"GPT 3.5 Turbo","object":"model","preview":false,"vendor":"Azure OpenAI","version":"gpt-3.5-turbo-0613"},{"capabilities":{"family":"gpt-4","limits":{"max_context_window_tokens":32768,"max_output_tokens":4096,"max_prompt_tokens":32768},"object":"model_capabilities","supports":{"tool_calls":true},"tokenizer":"cl100k_base","type":"chat"},"id":"gpt-4","model_picker_enabled":false,"name":"GPT 4","object":"model","preview":false,"vendor":"Azure OpenAI","version":"gpt-4-0613"},{"capabilities":{"family":"gpt-4","limits":{"max_context_window_tokens":32768,"max_output_tokens":4096,"max_prompt_tokens":32768},"object":"model_capabilities","supports":{"tool_calls":true},"tokenizer":"cl100k_base","type":"chat"},"id":"gpt-4-0613","model_picker_enabled":false,"name":"GPT 4","object":"model","preview":false,"vendor":"Azure OpenAI","version":"gpt-4-0613"},{"capabilities":{"family":"gpt-4o","limits":{"max_context_window_tokens":128000,"max_output_tokens":4096,"max_prompt_tokens":64000},"object":"model_capabilities","supports":{"parallel_tool_calls":true,"tool_calls":true},"tokenizer":"o200k_base","type":"chat"},"id":"gpt-4o","model_picker_enabled":true,"name":"GPT 4o","object":"model","preview":false,"vendor":"Azure OpenAI","version":"gpt-4o-2024-05-13"},{"capabilities":{"family":"gpt-4o","limits":{"max_context_window_tokens":128000,"max_output_tokens":4096,"max_prompt_tokens":64000},"object":"model_capabilities","supports":{"parallel_tool_calls":true,"tool_calls":true},"tokenizer":"o200k_base","type":"chat"},"id":"gpt-4o-2024-05-13","model_picker_enabled":false,"name":"GPT 4o","object":"model","preview":false,"vendor":"Azure OpenAI","version":"gpt-4o-2024-05-13"},{"capabilities":{"family":"gpt-4o","limits":{"max_context_window_tokens":128000,"max_output_tokens":4096,"max_prompt_tokens":64000},"object":"model_capabilities","supports":{"parallel_tool_calls":true,"tool_calls":true},"tokenizer":"o200k_base","type":"chat"},"id":"gpt-4-o-preview","model_picker_enabled":false,"name":"GPT 4o","object":"model","preview":false,"vendor":"Azure OpenAI","version":"gpt-4o-2024-05-13"},{"capabilities":{"family":"gpt-4o","limits":{"max_context_window_tokens":128000,"max_output_tokens":16384,"max_prompt_tokens":64000},"object":"model_capabilities","supports":{"parallel_tool_calls":true,"tool_calls":true},"tokenizer":"o200k_base","type":"chat"},"id":"gpt-4o-2024-08-06","model_picker_enabled":false,"name":"GPT 4o","object":"model","preview":false,"vendor":"Azure OpenAI","version":"gpt-4o-2024-08-06"},{"capabilities":{"family":"text-embedding-ada-002","limits":{"max_inputs":256},"object":"model_capabilities","supports":{},"tokenizer":"cl100k_base","type":"embeddings"},"id":"text-embedding-ada-002","model_picker_enabled":false,"name":"Embedding V2 Ada","object":"model","preview":false,"vendor":"Azure OpenAI","version":"text-embedding-ada-002"},{"capabilities":{"family":"text-embedding-3-small","limits":{"max_inputs":512},"object":"model_capabilities","supports":{"dimensions":true},"tokenizer":"cl100k_base","type":"embeddings"},"id":"text-embedding-3-small","model_picker_enabled":false,"name":"Embedding V3 small","object":"model","preview":false,"vendor":"Azure OpenAI","version":"text-embedding-3-small"},{"capabilities":{"family":"text-embedding-3-small","object":"model_capabilities","supports":{"dimensions":true},"tokenizer":"cl100k_base","type":"embeddings"},"id":"text-embedding-3-small-inference","model_picker_enabled":false,"name":"Embedding V3 small (Inference)","object":"model","preview":false,"vendor":"Azure OpenAI","version":"text-embedding-3-small"},{"capabilities":{"family":"gpt-4o-mini","limits":{"max_context_window_tokens":128000,"max_output_tokens":4096,"max_prompt_tokens":12288},"object":"model_capabilities","supports":{"parallel_tool_calls":true,"tool_calls":true},"tokenizer":"o200k_base","type":"chat"},"id":"gpt-4o-mini","model_picker_enabled":false,"name":"GPT 4o Mini","object":"model","preview":false,"vendor":"Azure OpenAI","version":"gpt-4o-mini-2024-07-18"},{"capabilities":{"family":"gpt-4o-mini","limits":{"max_context_window_tokens":128000,"max_output_tokens":4096,"max_prompt_tokens":12288},"object":"model_capabilities","supports":{"parallel_tool_calls":true,"tool_calls":true},"tokenizer":"o200k_base","type":"chat"},"id":"gpt-4o-mini-2024-07-18","model_picker_enabled":false,"name":"GPT 4o Mini","object":"model","preview":false,"vendor":"Azure OpenAI","version":"gpt-4o-mini-2024-07-18"},{"capabilities":{"family":"gpt-4-turbo","limits":{"max_context_window_tokens":128000,"max_output_tokens":4096,"max_prompt_tokens":64000},"object":"model_capabilities","supports":{"parallel_tool_calls":true,"tool_calls":true},"tokenizer":"cl100k_base","type":"chat"},"id":"gpt-4-0125-preview","model_picker_enabled":false,"name":"GPT 4 Turbo","object":"model","preview":false,"vendor":"Azure OpenAI","version":"gpt-4-0125-preview"},{"capabilities":{"family":"o1-mini","limits":{"max_context_window_tokens":128000,"max_prompt_tokens":20000},"object":"model_capabilities","supports":{},"tokenizer":"o200k_base","type":"chat"},"id":"o1-mini","model_picker_enabled":true,"name":"o1-mini (Preview)","object":"model","preview":true,"vendor":"Azure OpenAI","version":"o1-mini-2024-09-12"},{"capabilities":{"family":"o1-mini","limits":{"max_context_window_tokens":128000,"max_prompt_tokens":20000},"object":"model_capabilities","supports":{},"tokenizer":"o200k_base","type":"chat"},"id":"o1-mini-2024-09-12","model_picker_enabled":false,"name":"o1-mini (Preview)","object":"model","preview":true,"vendor":"Azure OpenAI","version":"o1-mini-2024-09-12"},{"capabilities":{"family":"o1-ga","limits":{"max_context_window_tokens":200000,"max_prompt_tokens":20000},"object":"model_capabilities","supports":{"tool_calls":true},"tokenizer":"o200k_base","type":"chat"},"id":"o1","model_picker_enabled":true,"name":"o1 (Preview)","object":"model","preview":true,"vendor":"Azure OpenAI","version":"o1-2024-12-17"},{"capabilities":{"family":"o1-ga","limits":{"max_context_window_tokens":200000,"max_prompt_tokens":20000},"object":"model_capabilities","supports":{"tool_calls":true},"tokenizer":"o200k_base","type":"chat"},"id":"o1-2024-12-17","model_picker_enabled":false,"name":"o1 (Preview)","object":"model","preview":true,"vendor":"Azure OpenAI","version":"o1-2024-12-17"},{"capabilities":{"family":"claude-3.5-sonnet","limits":{"max_context_window_tokens":200000,"max_output_tokens":4096,"max_prompt_tokens":195000},"object":"model_capabilities","supports":{"parallel_tool_calls":true,"tool_calls":true},"tokenizer":"o200k_base","type":"chat"},"id":"claude-3.5-sonnet","model_picker_enabled":true,"name":"Claude 3.5 Sonnet (Preview)","object":"model","policy":{"state":"enabled","terms":"Enable access to the latest Claude 3.5 Sonnet model from Anthropic. [Learn more about how GitHub Copilot serves Claude 3.5 Sonnet](https://docs.github.com/copilot/using-github-copilot/using-claude-sonnet-in-github-copilot)."},"preview":true,"vendor":"Anthropic","version":"claude-3.5-sonnet"}],"object":"list"}

src/lib/paths.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import os from "node:os"
2+
import path from "pathe"
3+
4+
const DIR_CACHE = path.join(os.homedir(), ".cache", "copilot-api")
5+
6+
const PATH_TOKEN_CACHE = path.join(DIR_CACHE, "token")
7+
8+
export const PATHS = {
9+
DIR_CACHE,
10+
PATH_TOKEN_CACHE,
11+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { ofetch } from "ofetch"
2+
3+
import { getToken } from "./get-token/service"
4+
5+
const result = await getToken()
6+
7+
export const COPILOT_VSCODE_BASE_URL =
8+
"https://api.individual.githubcopilot.com"
9+
export const COPILOT_VSCODE_TOKEN = result.token
10+
export const COPILOT_VSCODE_HEADERS = {
11+
authorization: `Bearer ${COPILOT_VSCODE_TOKEN}`,
12+
"copilot-integration-id": "vscode-chat",
13+
}
14+
15+
export const copilotVSCode = ofetch.create({
16+
baseURL: COPILOT_VSCODE_BASE_URL,
17+
headers: COPILOT_VSCODE_HEADERS,
18+
})
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { FetchError } from "ofetch"
2+
3+
import type { ChatCompletionsPayload } from "./types"
4+
5+
import {
6+
COPILOT_VSCODE_BASE_URL,
7+
COPILOT_VSCODE_HEADERS,
8+
copilotVSCode,
9+
} from "../api-instance"
10+
11+
export async function* chatCompletions(payload: ChatCompletionsPayload) {
12+
try {
13+
const response = await copilotVSCode.native(
14+
COPILOT_VSCODE_BASE_URL + "/chat/completions",
15+
{
16+
method: "POST",
17+
body: JSON.stringify(payload),
18+
headers: COPILOT_VSCODE_HEADERS,
19+
},
20+
)
21+
22+
console.log(await response.text())
23+
24+
// if (!response.body) {
25+
// throw new Error("No response body")
26+
// }
27+
28+
// const reader = response.body.getReader()
29+
30+
// // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
31+
// while (true) {
32+
// const { done, value } = await reader.read()
33+
34+
// if (done) {
35+
// console.log("done")
36+
// console.log(value)
37+
// break
38+
// }
39+
40+
// console.log("value", value)
41+
// }
42+
43+
// for await (const chunk of response.body) {
44+
// console.log(chunk)
45+
// }
46+
47+
yield "tono"
48+
} catch (e) {
49+
console.error(e)
50+
if (e instanceof FetchError) {
51+
console.error(e.response?._data)
52+
}
53+
}
54+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
// Request types
2+
3+
interface Message {
4+
role: string
5+
content: string
6+
}
7+
8+
export interface ChatCompletionsPayload {
9+
messages: Array<Message>
10+
model: string
11+
temperature?: number
12+
top_p?: number
13+
max_tokens?: number
14+
stop?: Array<string>
15+
n?: number
16+
stream?: boolean
17+
}
18+
19+
// Response types
20+
21+
interface ContentFilterResults {
22+
error: {
23+
code: string
24+
message: string
25+
}
26+
hate: {
27+
filtered: boolean
28+
severity: string
29+
}
30+
self_harm: {
31+
filtered: boolean
32+
severity: string
33+
}
34+
sexual: {
35+
filtered: boolean
36+
severity: string
37+
}
38+
violence: {
39+
filtered: boolean
40+
severity: string
41+
}
42+
}
43+
44+
interface ContentFilterOffsets {
45+
check_offset: number
46+
start_offset: number
47+
end_offset: number
48+
}
49+
50+
interface Delta {
51+
content: string | null
52+
role?: string
53+
}
54+
55+
interface Choice {
56+
index: number
57+
content_filter_offsets?: ContentFilterOffsets
58+
content_filter_results?: ContentFilterResults
59+
delta: Delta
60+
finish_reason?: string | null
61+
}
62+
63+
interface PromptFilterResult {
64+
content_filter_results: ContentFilterResults
65+
prompt_index: number
66+
}
67+
68+
interface Usage {
69+
completion_tokens: number
70+
prompt_tokens: number
71+
total_tokens: number
72+
}
73+
74+
interface ChatCompletionResponse {
75+
choices: Array<Choice>
76+
created: number
77+
id: string
78+
model: string
79+
system_fingerprint?: string
80+
prompt_filter_results?: Array<PromptFilterResult>
81+
usage?: Usage
82+
}
83+
84+
export type ChatCompletionsChunk =
85+
| { data: ChatCompletionResponse }
86+
| { data: "[DONE]" }
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import type { GetModelsResponse } from "./types"
2+
3+
import { copilotVSCode } from "../api-instance"
4+
5+
export const getModels = () =>
6+
copilotVSCode<GetModelsResponse>("/models", {
7+
method: "GET",
8+
})
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
interface ModelLimits {
2+
max_context_window_tokens?: number
3+
max_output_tokens?: number
4+
max_prompt_tokens?: number
5+
max_inputs?: number
6+
}
7+
8+
interface ModelSupports {
9+
tool_calls?: boolean
10+
parallel_tool_calls?: boolean
11+
dimensions?: boolean
12+
}
13+
14+
interface ModelCapabilities {
15+
family: string
16+
limits: ModelLimits
17+
object: string
18+
supports: ModelSupports
19+
tokenizer: string
20+
type: string
21+
}
22+
23+
interface Model {
24+
capabilities: ModelCapabilities
25+
id: string
26+
model_picker_enabled: boolean
27+
name: string
28+
object: string
29+
preview: boolean
30+
vendor: string
31+
version: string
32+
policy?: {
33+
state: string
34+
terms: string
35+
}
36+
}
37+
38+
export interface GetModelsResponse {
39+
data: Array<Model>
40+
object: string
41+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import consola from "consola"
2+
import { execa } from "execa"
3+
4+
import { PATHS } from "~/lib/paths"
5+
6+
import type { GetTokenResponse } from "./types"
7+
8+
const TEN_MINUTES = 10 * 60 * 1000
9+
10+
// @ts-expect-error TypeScript can't analyze timeout
11+
export async function getToken(): Promise<GetTokenResponse> {
12+
try {
13+
const cachedToken = await readCachedToken()
14+
15+
if (Date.now() - cachedToken.expires_at > ONE_DAY) {
16+
return cachedToken
17+
}
18+
} catch (e) {
19+
if (!(e instanceof Error)) throw e
20+
if (e.message === "No such file or directory")
21+
consola.info(`No cached token found in ${PATHS.PATH_TOKEN_CACHE}`)
22+
}
23+
24+
// Kill any existing vscode processes
25+
// otherwise, no token call will be made
26+
await killVSCodeProcesses()
27+
28+
const mitmdump = createMitmdumpProcess()
29+
void createVSCodeProcess()
30+
31+
const timeout = setTimeout(() => {
32+
throw new Error("Timed out waiting for token")
33+
}, 30_000)
34+
35+
for await (const line of mitmdump.stdout) {
36+
if (typeof line !== "string") continue
37+
if (!line.includes("tid=")) continue
38+
39+
consola.debug(`Found token output line: ${line}`)
40+
41+
clearTimeout(timeout)
42+
43+
await killVSCodeProcesses()
44+
mitmdump.kill()
45+
46+
const parsed = JSON.parse(line) as GetTokenResponse
47+
parsed.expires_at = Date.now() + t
48+
49+
await writeCachedToken(line)
50+
return JSON.parse(line) as GetTokenResponse
51+
}
52+
}
53+
54+
const createMitmdumpProcess = () =>
55+
execa({ reject: false })("mitmdump", [
56+
"--flow-detail",
57+
"4",
58+
"~m GET & ~u https://api.github.com/copilot_internal/v2/token",
59+
])
60+
61+
const createVSCodeProcess = () =>
62+
execa({
63+
reject: false,
64+
env: {
65+
http_proxy: "http://127.0.0.1:8080",
66+
https_proxy: "http://127.0.0.1:8080",
67+
},
68+
})("code", [
69+
"--ignore-certificate-errors",
70+
// Can be whatever folder, as long as it's trusted by vscode
71+
// https://code.visualstudio.com/docs/editor/workspace-trust
72+
"/home/erick/Documents/sides/playground/",
73+
])
74+
75+
const killVSCodeProcesses = () => execa({ reject: false })("pkill", ["code"])
76+
77+
const readCachedToken = async () => {
78+
const content = await Bun.file(PATHS.PATH_TOKEN_CACHE).text()
79+
return JSON.parse(content) as GetTokenResponse
80+
}
81+
82+
const writeCachedToken = async (token: string) =>
83+
Bun.write(PATHS.PATH_TOKEN_CACHE, token)
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
export interface GetTokenResponse {
2+
annotations_enabled: boolean
3+
chat_enabled: boolean
4+
chat_jetbrains_enabled: boolean
5+
code_quote_enabled: boolean
6+
code_review_enabled: boolean
7+
codesearch: boolean
8+
copilotignore_enabled: boolean
9+
endpoints: {
10+
api: string
11+
"origin-tracker": string
12+
proxy: string
13+
telemetry: string
14+
}
15+
expires_at: number
16+
individual: boolean
17+
limited_user_quotas: null
18+
limited_user_reset_date: null
19+
nes_enabled: boolean
20+
prompt_8k: boolean
21+
public_suggestions: "disabled"
22+
refresh_in: number
23+
sku: "free_educational"
24+
snippy_load_test_enabled: boolean
25+
telemetry: "disabled"
26+
token: string
27+
tracking_id: string
28+
vsc_electron_fetcher_v2: boolean
29+
xcode: boolean
30+
xcode_chat: boolean
31+
}

0 commit comments

Comments
 (0)