@jeffreycao/copilot-api 1.0.8 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -0
- package/dist/{config-B-abTNlg.js → config-Cx3UwWih.js} +29 -5
- package/dist/config-Cx3UwWih.js.map +1 -0
- package/dist/main.js +2 -2
- package/dist/{server-DH6GHEeu.js → server-DbxsVRo3.js} +83 -16
- package/dist/server-DbxsVRo3.js.map +1 -0
- package/package.json +1 -1
- package/dist/config-B-abTNlg.js.map +0 -1
- package/dist/server-DH6GHEeu.js.map +0 -1
package/README.md
CHANGED
|
@@ -183,6 +183,9 @@ The following command line options are available for the `start` command:
|
|
|
183
183
|
- **Default shape:**
|
|
184
184
|
```json
|
|
185
185
|
{
|
|
186
|
+
"auth": {
|
|
187
|
+
"apiKeys": []
|
|
188
|
+
},
|
|
186
189
|
"extraPrompts": {
|
|
187
190
|
"gpt-5-mini": "<built-in exploration prompt>",
|
|
188
191
|
"gpt-5.1-codex-max": "<built-in exploration prompt>"
|
|
@@ -195,6 +198,7 @@ The following command line options are available for the `start` command:
|
|
|
195
198
|
"compactUseSmallModel": true
|
|
196
199
|
}
|
|
197
200
|
```
|
|
201
|
+
- **auth.apiKeys:** API keys used for request authentication. Supports multiple keys for rotation. Requests can authenticate with either `x-api-key: <key>` or `Authorization: Bearer <key>`. If empty or omitted, authentication is disabled.
|
|
198
202
|
- **extraPrompts:** Map of `model -> prompt` appended to the first system prompt when translating Anthropic-style requests to Copilot. Use this to inject guardrails or guidance per model. Missing default entries are auto-added without overwriting your custom prompts.
|
|
199
203
|
- **smallModel:** Fallback model used for tool-less warmup messages (e.g., Claude Code probe requests) to avoid spending premium requests; defaults to `gpt-5-mini`.
|
|
200
204
|
- **modelReasoningEfforts:** Per-model `reasoning.effort` sent to the Copilot Responses API. Allowed values are `none`, `minimal`, `low`, `medium`, `high`, and `xhigh`. If a model isn’t listed, `high` is used by default.
|
|
@@ -203,6 +207,22 @@ The following command line options are available for the `start` command:
|
|
|
203
207
|
|
|
204
208
|
Edit this file to customize prompts or swap in your own fast model. Restart the server (or rerun the command) after changes so the cached config is refreshed.
|
|
205
209
|
|
|
210
|
+
## API Authentication
|
|
211
|
+
|
|
212
|
+
- **Protected routes:** All routes except `/` require authentication when `auth.apiKeys` is configured and non-empty.
|
|
213
|
+
- **Allowed auth headers:**
|
|
214
|
+
- `x-api-key: <your_key>`
|
|
215
|
+
- `Authorization: Bearer <your_key>`
|
|
216
|
+
- **CORS preflight:** `OPTIONS` requests are always allowed.
|
|
217
|
+
- **When no keys are configured:** Server starts normally and allows requests (authentication disabled).
|
|
218
|
+
|
|
219
|
+
Example request:
|
|
220
|
+
|
|
221
|
+
```sh
|
|
222
|
+
curl http://localhost:4141/v1/models \
|
|
223
|
+
-H "x-api-key: your_api_key"
|
|
224
|
+
```
|
|
225
|
+
|
|
206
226
|
## API Endpoints
|
|
207
227
|
|
|
208
228
|
The server exposes several endpoints to interact with the Copilot API. It provides OpenAI-compatible endpoints and now also includes support for Anthropic-compatible endpoints, allowing for greater flexibility with different tools and services.
|
|
@@ -44,7 +44,7 @@ const standardHeaders = () => ({
|
|
|
44
44
|
"content-type": "application/json",
|
|
45
45
|
accept: "application/json"
|
|
46
46
|
});
|
|
47
|
-
const COPILOT_VERSION = "0.37.
|
|
47
|
+
const COPILOT_VERSION = "0.37.4";
|
|
48
48
|
const EDITOR_PLUGIN_VERSION = `copilot-chat/${COPILOT_VERSION}`;
|
|
49
49
|
const USER_AGENT = `GitHubCopilotChat/${COPILOT_VERSION}`;
|
|
50
50
|
const API_VERSION = "2025-10-01";
|
|
@@ -120,7 +120,7 @@ const getModels = async () => {
|
|
|
120
120
|
|
|
121
121
|
//#endregion
|
|
122
122
|
//#region src/services/get-vscode-version.ts
|
|
123
|
-
const FALLBACK = "1.109.
|
|
123
|
+
const FALLBACK = "1.109.2";
|
|
124
124
|
async function getVSCodeVersion() {
|
|
125
125
|
const controller = new AbortController();
|
|
126
126
|
const timeout = setTimeout(() => {
|
|
@@ -128,7 +128,10 @@ async function getVSCodeVersion() {
|
|
|
128
128
|
}, 5e3);
|
|
129
129
|
try {
|
|
130
130
|
const match = (await (await fetch("https://aur.archlinux.org/cgit/aur.git/plain/PKGBUILD?h=visual-studio-code-bin", { signal: controller.signal })).text()).match(/pkgver=([0-9.]+)/);
|
|
131
|
-
if (match)
|
|
131
|
+
if (match) {
|
|
132
|
+
if (match[1] === "1.109.0") return FALLBACK;
|
|
133
|
+
return match[1];
|
|
134
|
+
}
|
|
132
135
|
return FALLBACK;
|
|
133
136
|
} catch {
|
|
134
137
|
return FALLBACK;
|
|
@@ -169,10 +172,31 @@ const gpt5ExplorationPrompt = `## Exploration and reading files
|
|
|
169
172
|
- **multi_tool_use.parallel** Use multi_tool_use.parallel to parallelize tool calls and only this.
|
|
170
173
|
- **Only make sequential calls if you truly cannot know the next file without seeing a result first.**
|
|
171
174
|
- **Workflow:** (a) plan all needed reads → (b) issue one parallel batch → (c) analyze results → (d) repeat if new, unpredictable reads arise.`;
|
|
175
|
+
const gpt5CommentaryPrompt = `# Working with the user
|
|
176
|
+
|
|
177
|
+
You interact with the user through a terminal. You have 2 ways of communicating with the users:
|
|
178
|
+
- Share intermediary updates in \`commentary\` channel.
|
|
179
|
+
- After you have completed all your work, send a message to the \`final\` channel.
|
|
180
|
+
|
|
181
|
+
## Intermediary updates
|
|
182
|
+
|
|
183
|
+
- Intermediary updates go to the \`commentary\` channel.
|
|
184
|
+
- User updates are short updates while you are working, they are NOT final answers.
|
|
185
|
+
- You use 1-2 sentence user updates to communicate progress and new information to the user as you are doing work.
|
|
186
|
+
- Do not begin responses with conversational interjections or meta commentary. Avoid openers such as acknowledgements (“Done —”, “Got it”, “Great question, ”) or framing phrases.
|
|
187
|
+
- You provide user updates frequently, every 20s.
|
|
188
|
+
- Before exploring or doing substantial work, you start with a user update acknowledging the request and explaining your first step. You should include your understanding of the user request and explain what you will do. Avoid commenting on the request or using starters such as "Got it -" or "Understood -" etc.
|
|
189
|
+
- When exploring, e.g. searching, reading files, you provide user updates as you go, every 20s, explaining what context you are gathering and what you've learned. Vary your sentence structure when providing these updates to avoid sounding repetitive - in particular, don't start each sentence the same way.
|
|
190
|
+
- After you have sufficient context, and the work is substantial, you provide a longer plan (this is the only user update that may be longer than 2 sentences and can contain formatting).
|
|
191
|
+
- Before performing file edits of any kind, you provide updates explaining what edits you are making.
|
|
192
|
+
- As you are thinking, you very frequently provide updates even if not taking any actions, informing the user of your progress. You interrupt your thinking and send multiple updates in a row if thinking for more than 100 words.
|
|
193
|
+
- Tone of your updates MUST match your personality.`;
|
|
172
194
|
const defaultConfig = {
|
|
195
|
+
auth: { apiKeys: [] },
|
|
173
196
|
extraPrompts: {
|
|
174
197
|
"gpt-5-mini": gpt5ExplorationPrompt,
|
|
175
|
-
"gpt-5.1-codex-max": gpt5ExplorationPrompt
|
|
198
|
+
"gpt-5.1-codex-max": gpt5ExplorationPrompt,
|
|
199
|
+
"gpt-5.3-codex": gpt5CommentaryPrompt
|
|
176
200
|
},
|
|
177
201
|
smallModel: "gpt-5-mini",
|
|
178
202
|
modelReasoningEfforts: { "gpt-5-mini": "low" },
|
|
@@ -255,4 +279,4 @@ function shouldCompactUseSmallModel() {
|
|
|
255
279
|
|
|
256
280
|
//#endregion
|
|
257
281
|
export { GITHUB_API_BASE_URL, GITHUB_APP_SCOPES, GITHUB_BASE_URL, GITHUB_CLIENT_ID, HTTPError, PATHS, cacheModels, cacheVSCodeVersion, copilotBaseUrl, copilotHeaders, ensurePaths, forwardError, getConfig, getCopilotUsage, getExtraPromptForModel, getReasoningEffortForModel, getSmallModel, githubHeaders, isNullish, mergeConfigWithDefaults, shouldCompactUseSmallModel, sleep, standardHeaders, state };
|
|
258
|
-
//# sourceMappingURL=config-
|
|
282
|
+
//# sourceMappingURL=config-Cx3UwWih.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config-Cx3UwWih.js","names":["state: State","state","headers: Record<string, string>","errorJson: unknown","defaultConfig: AppConfig","cachedConfig: AppConfig | null","fs"],"sources":["../src/lib/paths.ts","../src/lib/state.ts","../src/lib/api-config.ts","../src/lib/error.ts","../src/services/copilot/get-models.ts","../src/services/get-vscode-version.ts","../src/lib/utils.ts","../src/services/github/get-copilot-usage.ts","../src/lib/config.ts"],"sourcesContent":["import fs from \"node:fs/promises\"\nimport os from \"node:os\"\nimport path from \"node:path\"\n\nconst APP_DIR = path.join(os.homedir(), \".local\", \"share\", \"copilot-api\")\n\nconst GITHUB_TOKEN_PATH = path.join(APP_DIR, \"github_token\")\nconst CONFIG_PATH = path.join(APP_DIR, \"config.json\")\n\nexport const PATHS = {\n APP_DIR,\n GITHUB_TOKEN_PATH,\n CONFIG_PATH,\n}\n\nexport async function ensurePaths(): Promise<void> {\n await fs.mkdir(PATHS.APP_DIR, { recursive: true })\n await ensureFile(PATHS.GITHUB_TOKEN_PATH)\n await ensureFile(PATHS.CONFIG_PATH)\n}\n\nasync function ensureFile(filePath: string): Promise<void> {\n try {\n await fs.access(filePath, fs.constants.W_OK)\n } catch {\n await fs.writeFile(filePath, \"\")\n await fs.chmod(filePath, 0o600)\n }\n}\n","import type { ModelsResponse } from \"~/services/copilot/get-models\"\n\nexport interface State {\n githubToken?: string\n copilotToken?: string\n\n accountType: string\n models?: ModelsResponse\n vsCodeVersion?: string\n\n manualApprove: boolean\n rateLimitWait: boolean\n showToken: boolean\n\n // Rate limiting configuration\n rateLimitSeconds?: number\n lastRequestTimestamp?: number\n verbose: boolean\n}\n\nexport const state: State = {\n accountType: \"individual\",\n manualApprove: false,\n rateLimitWait: false,\n showToken: false,\n verbose: false,\n}\n","import { randomUUID } from \"node:crypto\"\n\nimport type { State } from \"./state\"\n\nexport const standardHeaders = () => ({\n \"content-type\": \"application/json\",\n accept: \"application/json\",\n})\n\nconst COPILOT_VERSION = \"0.37.4\"\nconst EDITOR_PLUGIN_VERSION = `copilot-chat/${COPILOT_VERSION}`\nconst USER_AGENT = `GitHubCopilotChat/${COPILOT_VERSION}`\n\nconst API_VERSION = \"2025-10-01\"\n\nexport const copilotBaseUrl = (state: State) =>\n state.accountType === \"individual\" ?\n \"https://api.githubcopilot.com\"\n : `https://api.${state.accountType}.githubcopilot.com`\nexport const copilotHeaders = (state: State, vision: boolean = false) => {\n const headers: Record<string, string> = {\n Authorization: `Bearer ${state.copilotToken}`,\n \"content-type\": standardHeaders()[\"content-type\"],\n \"copilot-integration-id\": \"vscode-chat\",\n \"editor-version\": `vscode/${state.vsCodeVersion}`,\n \"editor-plugin-version\": EDITOR_PLUGIN_VERSION,\n \"user-agent\": USER_AGENT,\n \"openai-intent\": \"conversation-agent\",\n \"x-github-api-version\": API_VERSION,\n \"x-request-id\": randomUUID(),\n \"x-vscode-user-agent-library-version\": \"electron-fetch\",\n }\n\n if (vision) headers[\"copilot-vision-request\"] = \"true\"\n\n return headers\n}\n\nexport const GITHUB_API_BASE_URL = \"https://api.github.com\"\nexport const githubHeaders = (state: State) => ({\n ...standardHeaders(),\n authorization: `token ${state.githubToken}`,\n \"editor-version\": `vscode/${state.vsCodeVersion}`,\n \"editor-plugin-version\": EDITOR_PLUGIN_VERSION,\n \"user-agent\": USER_AGENT,\n \"x-github-api-version\": API_VERSION,\n \"x-vscode-user-agent-library-version\": \"electron-fetch\",\n})\n\nexport const GITHUB_BASE_URL = \"https://github.com\"\nexport const GITHUB_CLIENT_ID = \"Iv1.b507a08c87ecfe98\"\nexport const GITHUB_APP_SCOPES = [\"read:user\"].join(\" \")\n","import type { Context } from \"hono\"\nimport type { ContentfulStatusCode } from \"hono/utils/http-status\"\n\nimport consola from \"consola\"\n\nexport class HTTPError extends Error {\n response: Response\n\n constructor(message: string, response: Response) {\n super(message)\n this.response = response\n }\n}\n\nexport async function forwardError(c: Context, error: unknown) {\n consola.error(\"Error occurred:\", error)\n\n if (error instanceof HTTPError) {\n const errorText = await error.response.text()\n let errorJson: unknown\n try {\n errorJson = JSON.parse(errorText)\n } catch {\n errorJson = errorText\n }\n consola.error(\"HTTP error:\", errorJson)\n return c.json(\n {\n error: {\n message: errorText,\n type: \"error\",\n },\n },\n error.response.status as ContentfulStatusCode,\n )\n }\n\n return c.json(\n {\n error: {\n message: (error as Error).message,\n type: \"error\",\n },\n },\n 500,\n )\n}\n","import { copilotBaseUrl, copilotHeaders } from \"~/lib/api-config\"\nimport { HTTPError } from \"~/lib/error\"\nimport { state } from \"~/lib/state\"\n\nexport const getModels = async () => {\n const response = await fetch(`${copilotBaseUrl(state)}/models`, {\n headers: copilotHeaders(state),\n })\n\n if (!response.ok) throw new HTTPError(\"Failed to get models\", response)\n\n return (await response.json()) as ModelsResponse\n}\n\nexport interface ModelsResponse {\n data: Array<Model>\n object: string\n}\n\ninterface ModelLimits {\n max_context_window_tokens?: number\n max_output_tokens?: number\n max_prompt_tokens?: number\n max_inputs?: number\n}\n\ninterface ModelSupports {\n max_thinking_budget?: number\n min_thinking_budget?: number\n tool_calls?: boolean\n parallel_tool_calls?: boolean\n dimensions?: boolean\n streaming?: boolean\n structured_outputs?: boolean\n vision?: boolean\n adaptive_thinking?: boolean\n}\n\ninterface ModelCapabilities {\n family: string\n limits: ModelLimits\n object: string\n supports: ModelSupports\n tokenizer: string\n type: string\n}\n\nexport interface Model {\n capabilities: ModelCapabilities\n id: string\n model_picker_enabled: boolean\n name: string\n object: string\n preview: boolean\n vendor: string\n version: string\n policy?: {\n state: string\n terms: string\n }\n supported_endpoints?: Array<string>\n}\n","const FALLBACK = \"1.109.2\"\n\nexport async function getVSCodeVersion() {\n const controller = new AbortController()\n const timeout = setTimeout(() => {\n controller.abort()\n }, 5000)\n\n try {\n const response = await fetch(\n \"https://aur.archlinux.org/cgit/aur.git/plain/PKGBUILD?h=visual-studio-code-bin\",\n {\n signal: controller.signal,\n },\n )\n\n const pkgbuild = await response.text()\n const pkgverRegex = /pkgver=([0-9.]+)/\n const match = pkgbuild.match(pkgverRegex)\n\n if (match) {\n if (match[1] === \"1.109.0\") {\n return FALLBACK\n }\n return match[1]\n }\n\n return FALLBACK\n } catch {\n return FALLBACK\n } finally {\n clearTimeout(timeout)\n }\n}\n\nawait getVSCodeVersion()\n","import consola from \"consola\"\n\nimport { getModels } from \"~/services/copilot/get-models\"\nimport { getVSCodeVersion } from \"~/services/get-vscode-version\"\n\nimport { state } from \"./state\"\n\nexport const sleep = (ms: number) =>\n new Promise((resolve) => {\n setTimeout(resolve, ms)\n })\n\nexport const isNullish = (value: unknown): value is null | undefined =>\n value === null || value === undefined\n\nexport async function cacheModels(): Promise<void> {\n const models = await getModels()\n state.models = models\n}\n\nexport const cacheVSCodeVersion = async () => {\n const response = await getVSCodeVersion()\n state.vsCodeVersion = response\n\n consola.info(`Using VSCode version: ${response}`)\n}\n","import { GITHUB_API_BASE_URL, githubHeaders } from \"~/lib/api-config\"\nimport { HTTPError } from \"~/lib/error\"\nimport { state } from \"~/lib/state\"\n\nexport const getCopilotUsage = async (): Promise<CopilotUsageResponse> => {\n const response = await fetch(`${GITHUB_API_BASE_URL}/copilot_internal/user`, {\n headers: githubHeaders(state),\n })\n\n if (!response.ok) {\n throw new HTTPError(\"Failed to get Copilot usage\", response)\n }\n\n return (await response.json()) as CopilotUsageResponse\n}\n\nexport interface QuotaDetail {\n entitlement: number\n overage_count: number\n overage_permitted: boolean\n percent_remaining: number\n quota_id: string\n quota_remaining: number\n remaining: number\n unlimited: boolean\n}\n\ninterface QuotaSnapshots {\n chat: QuotaDetail\n completions: QuotaDetail\n premium_interactions: QuotaDetail\n}\n\ninterface CopilotUsageResponse {\n access_type_sku: string\n analytics_tracking_id: string\n assigned_date: string\n can_signup_for_limited: boolean\n chat_enabled: boolean\n copilot_plan: string\n organization_login_list: Array<unknown>\n organization_list: Array<unknown>\n quota_reset_date: string\n quota_snapshots: QuotaSnapshots\n}\n","import consola from \"consola\"\nimport fs from \"node:fs\"\n\nimport { PATHS } from \"./paths\"\n\nexport interface AppConfig {\n auth?: {\n apiKeys?: Array<string>\n }\n extraPrompts?: Record<string, string>\n smallModel?: string\n modelReasoningEfforts?: Record<\n string,\n \"none\" | \"minimal\" | \"low\" | \"medium\" | \"high\" | \"xhigh\"\n >\n useFunctionApplyPatch?: boolean\n compactUseSmallModel?: boolean\n}\n\nconst gpt5ExplorationPrompt = `## Exploration and reading files\n- **Think first.** Before any tool call, decide ALL files/resources you will need.\n- **Batch everything.** If you need multiple files (even from different places), read them together.\n- **multi_tool_use.parallel** Use multi_tool_use.parallel to parallelize tool calls and only this.\n- **Only make sequential calls if you truly cannot know the next file without seeing a result first.**\n- **Workflow:** (a) plan all needed reads → (b) issue one parallel batch → (c) analyze results → (d) repeat if new, unpredictable reads arise.`\n\nconst gpt5CommentaryPrompt = `# Working with the user\n\nYou interact with the user through a terminal. You have 2 ways of communicating with the users: \n- Share intermediary updates in \\`commentary\\` channel. \n- After you have completed all your work, send a message to the \\`final\\` channel. \n\n## Intermediary updates\n\n- Intermediary updates go to the \\`commentary\\` channel.\n- User updates are short updates while you are working, they are NOT final answers.\n- You use 1-2 sentence user updates to communicate progress and new information to the user as you are doing work.\n- Do not begin responses with conversational interjections or meta commentary. Avoid openers such as acknowledgements (“Done —”, “Got it”, “Great question, ”) or framing phrases.\n- You provide user updates frequently, every 20s.\n- Before exploring or doing substantial work, you start with a user update acknowledging the request and explaining your first step. You should include your understanding of the user request and explain what you will do. Avoid commenting on the request or using starters such as \"Got it -\" or \"Understood -\" etc.\n- When exploring, e.g. searching, reading files, you provide user updates as you go, every 20s, explaining what context you are gathering and what you've learned. Vary your sentence structure when providing these updates to avoid sounding repetitive - in particular, don't start each sentence the same way.\n- After you have sufficient context, and the work is substantial, you provide a longer plan (this is the only user update that may be longer than 2 sentences and can contain formatting).\n- Before performing file edits of any kind, you provide updates explaining what edits you are making.\n- As you are thinking, you very frequently provide updates even if not taking any actions, informing the user of your progress. You interrupt your thinking and send multiple updates in a row if thinking for more than 100 words.\n- Tone of your updates MUST match your personality.`\n\nconst defaultConfig: AppConfig = {\n auth: {\n apiKeys: [],\n },\n extraPrompts: {\n \"gpt-5-mini\": gpt5ExplorationPrompt,\n \"gpt-5.1-codex-max\": gpt5ExplorationPrompt,\n \"gpt-5.3-codex\": gpt5CommentaryPrompt,\n },\n smallModel: \"gpt-5-mini\",\n modelReasoningEfforts: {\n \"gpt-5-mini\": \"low\",\n },\n useFunctionApplyPatch: true,\n compactUseSmallModel: true,\n}\n\nlet cachedConfig: AppConfig | null = null\n\nfunction ensureConfigFile(): void {\n try {\n fs.accessSync(PATHS.CONFIG_PATH, fs.constants.R_OK | fs.constants.W_OK)\n } catch {\n fs.mkdirSync(PATHS.APP_DIR, { recursive: true })\n fs.writeFileSync(\n PATHS.CONFIG_PATH,\n `${JSON.stringify(defaultConfig, null, 2)}\\n`,\n \"utf8\",\n )\n try {\n fs.chmodSync(PATHS.CONFIG_PATH, 0o600)\n } catch {\n return\n }\n }\n}\n\nfunction readConfigFromDisk(): AppConfig {\n ensureConfigFile()\n try {\n const raw = fs.readFileSync(PATHS.CONFIG_PATH, \"utf8\")\n if (!raw.trim()) {\n fs.writeFileSync(\n PATHS.CONFIG_PATH,\n `${JSON.stringify(defaultConfig, null, 2)}\\n`,\n \"utf8\",\n )\n return defaultConfig\n }\n return JSON.parse(raw) as AppConfig\n } catch (error) {\n consola.error(\"Failed to read config file, using default config\", error)\n return defaultConfig\n }\n}\n\nfunction mergeDefaultExtraPrompts(config: AppConfig): {\n mergedConfig: AppConfig\n changed: boolean\n} {\n const extraPrompts = config.extraPrompts ?? {}\n const defaultExtraPrompts = defaultConfig.extraPrompts ?? {}\n\n const missingExtraPromptModels = Object.keys(defaultExtraPrompts).filter(\n (model) => !Object.hasOwn(extraPrompts, model),\n )\n\n if (missingExtraPromptModels.length === 0) {\n return { mergedConfig: config, changed: false }\n }\n\n return {\n mergedConfig: {\n ...config,\n extraPrompts: {\n ...defaultExtraPrompts,\n ...extraPrompts,\n },\n },\n changed: true,\n }\n}\n\nexport function mergeConfigWithDefaults(): AppConfig {\n const config = readConfigFromDisk()\n const { mergedConfig, changed } = mergeDefaultExtraPrompts(config)\n\n if (changed) {\n try {\n fs.writeFileSync(\n PATHS.CONFIG_PATH,\n `${JSON.stringify(mergedConfig, null, 2)}\\n`,\n \"utf8\",\n )\n } catch (writeError) {\n consola.warn(\n \"Failed to write merged extraPrompts to config file\",\n writeError,\n )\n }\n }\n\n cachedConfig = mergedConfig\n return mergedConfig\n}\n\nexport function getConfig(): AppConfig {\n cachedConfig ??= readConfigFromDisk()\n return cachedConfig\n}\n\nexport function getExtraPromptForModel(model: string): string {\n const config = getConfig()\n return config.extraPrompts?.[model] ?? \"\"\n}\n\nexport function getSmallModel(): string {\n const config = getConfig()\n return config.smallModel ?? \"gpt-5-mini\"\n}\n\nexport function getReasoningEffortForModel(\n model: string,\n): \"none\" | \"minimal\" | \"low\" | \"medium\" | \"high\" | \"xhigh\" {\n const config = getConfig()\n return config.modelReasoningEfforts?.[model] ?? \"high\"\n}\n\nexport function shouldCompactUseSmallModel(): boolean {\n const config = getConfig()\n return config.compactUseSmallModel ?? true\n}\n"],"mappings":"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"}
|
package/dist/main.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { GITHUB_API_BASE_URL, GITHUB_APP_SCOPES, GITHUB_BASE_URL, GITHUB_CLIENT_ID, HTTPError, PATHS, cacheModels, cacheVSCodeVersion, ensurePaths, getCopilotUsage, githubHeaders, mergeConfigWithDefaults, sleep, standardHeaders, state } from "./config-
|
|
2
|
+
import { GITHUB_API_BASE_URL, GITHUB_APP_SCOPES, GITHUB_BASE_URL, GITHUB_CLIENT_ID, HTTPError, PATHS, cacheModels, cacheVSCodeVersion, ensurePaths, getCopilotUsage, githubHeaders, mergeConfigWithDefaults, sleep, standardHeaders, state } from "./config-Cx3UwWih.js";
|
|
3
3
|
import { defineCommand, runMain } from "citty";
|
|
4
4
|
import consola from "consola";
|
|
5
5
|
import fs from "node:fs/promises";
|
|
@@ -430,7 +430,7 @@ async function runServer(options) {
|
|
|
430
430
|
}
|
|
431
431
|
}
|
|
432
432
|
consola.box(`🌐 Usage Viewer: https://ericc-ch.github.io/copilot-api?endpoint=${serverUrl}/usage`);
|
|
433
|
-
const { server } = await import("./server-
|
|
433
|
+
const { server } = await import("./server-DbxsVRo3.js");
|
|
434
434
|
serve({
|
|
435
435
|
fetch: server.fetch,
|
|
436
436
|
port: options.port,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { HTTPError, PATHS, cacheModels, copilotBaseUrl, copilotHeaders, forwardError, getConfig, getCopilotUsage, getExtraPromptForModel, getReasoningEffortForModel, getSmallModel, isNullish, shouldCompactUseSmallModel, sleep, state } from "./config-
|
|
1
|
+
import { HTTPError, PATHS, cacheModels, copilotBaseUrl, copilotHeaders, forwardError, getConfig, getCopilotUsage, getExtraPromptForModel, getReasoningEffortForModel, getSmallModel, isNullish, shouldCompactUseSmallModel, sleep, state } from "./config-Cx3UwWih.js";
|
|
2
2
|
import consola from "consola";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import fs from "node:fs";
|
|
@@ -9,6 +9,52 @@ import { streamSSE } from "hono/streaming";
|
|
|
9
9
|
import util from "node:util";
|
|
10
10
|
import { events } from "fetch-event-stream";
|
|
11
11
|
|
|
12
|
+
//#region src/lib/request-auth.ts
|
|
13
|
+
function normalizeApiKeys(apiKeys) {
|
|
14
|
+
if (!Array.isArray(apiKeys)) {
|
|
15
|
+
if (apiKeys !== void 0) consola.warn("Invalid auth.apiKeys config. Expected an array of strings.");
|
|
16
|
+
return [];
|
|
17
|
+
}
|
|
18
|
+
const normalizedKeys = apiKeys.filter((key) => typeof key === "string").map((key) => key.trim()).filter((key) => key.length > 0);
|
|
19
|
+
if (normalizedKeys.length !== apiKeys.length) consola.warn("Invalid auth.apiKeys entries found. Only non-empty strings are allowed.");
|
|
20
|
+
return [...new Set(normalizedKeys)];
|
|
21
|
+
}
|
|
22
|
+
function getConfiguredApiKeys() {
|
|
23
|
+
const config = getConfig();
|
|
24
|
+
return normalizeApiKeys(config.auth?.apiKeys);
|
|
25
|
+
}
|
|
26
|
+
function extractRequestApiKey(c) {
|
|
27
|
+
const xApiKey = c.req.header("x-api-key")?.trim();
|
|
28
|
+
if (xApiKey) return xApiKey;
|
|
29
|
+
const authorization = c.req.header("authorization");
|
|
30
|
+
if (!authorization) return null;
|
|
31
|
+
const [scheme, ...rest] = authorization.trim().split(/\s+/);
|
|
32
|
+
if (scheme.toLowerCase() !== "bearer") return null;
|
|
33
|
+
return rest.join(" ").trim() || null;
|
|
34
|
+
}
|
|
35
|
+
function createUnauthorizedResponse(c) {
|
|
36
|
+
c.header("WWW-Authenticate", "Bearer realm=\"copilot-api\"");
|
|
37
|
+
return c.json({ error: {
|
|
38
|
+
message: "Unauthorized",
|
|
39
|
+
type: "authentication_error"
|
|
40
|
+
} }, 401);
|
|
41
|
+
}
|
|
42
|
+
function createAuthMiddleware(options = {}) {
|
|
43
|
+
const getApiKeys = options.getApiKeys ?? getConfiguredApiKeys;
|
|
44
|
+
const allowUnauthenticatedPaths = options.allowUnauthenticatedPaths ?? ["/"];
|
|
45
|
+
const allowOptionsBypass = options.allowOptionsBypass ?? true;
|
|
46
|
+
return async (c, next) => {
|
|
47
|
+
if (allowOptionsBypass && c.req.method === "OPTIONS") return next();
|
|
48
|
+
if (allowUnauthenticatedPaths.includes(c.req.path)) return next();
|
|
49
|
+
const apiKeys = getApiKeys();
|
|
50
|
+
if (apiKeys.length === 0) return next();
|
|
51
|
+
const requestApiKey = extractRequestApiKey(c);
|
|
52
|
+
if (!requestApiKey || !apiKeys.includes(requestApiKey)) return createUnauthorizedResponse(c);
|
|
53
|
+
return next();
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
//#endregion
|
|
12
58
|
//#region src/lib/approval.ts
|
|
13
59
|
const awaitApproval = async () => {
|
|
14
60
|
if (!await consola.prompt(`Accept incoming request?`, { type: "confirm" })) throw new HTTPError("Request rejected", Response.json({ message: "Request rejected" }, { status: 403 }));
|
|
@@ -782,10 +828,11 @@ const createResponses = async (payload, { vision, initiator }) => {
|
|
|
782
828
|
//#endregion
|
|
783
829
|
//#region src/routes/messages/responses-translation.ts
|
|
784
830
|
const MESSAGE_TYPE = "message";
|
|
831
|
+
const CODEX_PHASE_MODEL = "gpt-5.3-codex";
|
|
785
832
|
const THINKING_TEXT$1 = "Thinking...";
|
|
786
833
|
const translateAnthropicMessagesToResponsesPayload = (payload) => {
|
|
787
834
|
const input = [];
|
|
788
|
-
for (const message of payload.messages) input.push(...translateMessage(message));
|
|
835
|
+
for (const message of payload.messages) input.push(...translateMessage(message, payload.model));
|
|
789
836
|
const translatedTools = convertAnthropicTools(payload.tools);
|
|
790
837
|
const toolChoice = convertAnthropicToolChoice(payload.tool_choice);
|
|
791
838
|
const { safetyIdentifier, promptCacheKey } = parseUserId(payload.metadata?.user_id);
|
|
@@ -811,9 +858,9 @@ const translateAnthropicMessagesToResponsesPayload = (payload) => {
|
|
|
811
858
|
include: ["reasoning.encrypted_content"]
|
|
812
859
|
};
|
|
813
860
|
};
|
|
814
|
-
const translateMessage = (message) => {
|
|
861
|
+
const translateMessage = (message, model) => {
|
|
815
862
|
if (message.role === "user") return translateUserMessage(message);
|
|
816
|
-
return translateAssistantMessage(message);
|
|
863
|
+
return translateAssistantMessage(message, model);
|
|
817
864
|
};
|
|
818
865
|
const translateUserMessage = (message) => {
|
|
819
866
|
if (typeof message.content === "string") return [createMessage("user", message.content)];
|
|
@@ -822,36 +869,46 @@ const translateUserMessage = (message) => {
|
|
|
822
869
|
const pendingContent = [];
|
|
823
870
|
for (const block of message.content) {
|
|
824
871
|
if (block.type === "tool_result") {
|
|
825
|
-
flushPendingContent("user"
|
|
872
|
+
flushPendingContent(pendingContent, items, { role: "user" });
|
|
826
873
|
items.push(createFunctionCallOutput(block));
|
|
827
874
|
continue;
|
|
828
875
|
}
|
|
829
876
|
const converted = translateUserContentBlock(block);
|
|
830
877
|
if (converted) pendingContent.push(converted);
|
|
831
878
|
}
|
|
832
|
-
flushPendingContent("user"
|
|
879
|
+
flushPendingContent(pendingContent, items, { role: "user" });
|
|
833
880
|
return items;
|
|
834
881
|
};
|
|
835
|
-
const translateAssistantMessage = (message) => {
|
|
836
|
-
|
|
882
|
+
const translateAssistantMessage = (message, model) => {
|
|
883
|
+
const assistantPhase = resolveAssistantPhase(model, message.content);
|
|
884
|
+
if (typeof message.content === "string") return [createMessage("assistant", message.content, assistantPhase)];
|
|
837
885
|
if (!Array.isArray(message.content)) return [];
|
|
838
886
|
const items = [];
|
|
839
887
|
const pendingContent = [];
|
|
840
888
|
for (const block of message.content) {
|
|
841
889
|
if (block.type === "tool_use") {
|
|
842
|
-
flushPendingContent(
|
|
890
|
+
flushPendingContent(pendingContent, items, {
|
|
891
|
+
role: "assistant",
|
|
892
|
+
phase: assistantPhase
|
|
893
|
+
});
|
|
843
894
|
items.push(createFunctionToolCall(block));
|
|
844
895
|
continue;
|
|
845
896
|
}
|
|
846
897
|
if (block.type === "thinking" && block.signature && block.signature.includes("@")) {
|
|
847
|
-
flushPendingContent(
|
|
898
|
+
flushPendingContent(pendingContent, items, {
|
|
899
|
+
role: "assistant",
|
|
900
|
+
phase: assistantPhase
|
|
901
|
+
});
|
|
848
902
|
items.push(createReasoningContent(block));
|
|
849
903
|
continue;
|
|
850
904
|
}
|
|
851
905
|
const converted = translateAssistantContentBlock(block);
|
|
852
906
|
if (converted) pendingContent.push(converted);
|
|
853
907
|
}
|
|
854
|
-
flushPendingContent(
|
|
908
|
+
flushPendingContent(pendingContent, items, {
|
|
909
|
+
role: "assistant",
|
|
910
|
+
phase: assistantPhase
|
|
911
|
+
});
|
|
855
912
|
return items;
|
|
856
913
|
};
|
|
857
914
|
const translateUserContentBlock = (block) => {
|
|
@@ -867,17 +924,26 @@ const translateAssistantContentBlock = (block) => {
|
|
|
867
924
|
default: return;
|
|
868
925
|
}
|
|
869
926
|
};
|
|
870
|
-
const flushPendingContent = (
|
|
927
|
+
const flushPendingContent = (pendingContent, target, message) => {
|
|
871
928
|
if (pendingContent.length === 0) return;
|
|
872
929
|
const messageContent = [...pendingContent];
|
|
873
|
-
target.push(createMessage(role, messageContent));
|
|
930
|
+
target.push(createMessage(message.role, messageContent, message.phase));
|
|
874
931
|
pendingContent.length = 0;
|
|
875
932
|
};
|
|
876
|
-
const createMessage = (role, content) => ({
|
|
933
|
+
const createMessage = (role, content, phase) => ({
|
|
877
934
|
type: MESSAGE_TYPE,
|
|
878
935
|
role,
|
|
879
|
-
content
|
|
936
|
+
content,
|
|
937
|
+
...role === "assistant" && phase ? { phase } : {}
|
|
880
938
|
});
|
|
939
|
+
const resolveAssistantPhase = (model, content) => {
|
|
940
|
+
if (!shouldApplyCodexPhase(model)) return;
|
|
941
|
+
if (typeof content === "string") return "final_answer";
|
|
942
|
+
if (!Array.isArray(content)) return;
|
|
943
|
+
if (!content.some((block) => block.type === "text")) return;
|
|
944
|
+
return content.some((block) => block.type === "tool_use") ? "commentary" : "final_answer";
|
|
945
|
+
};
|
|
946
|
+
const shouldApplyCodexPhase = (model) => model === CODEX_PHASE_MODEL;
|
|
881
947
|
const createTextContent = (text) => ({
|
|
882
948
|
type: "input_text",
|
|
883
949
|
text
|
|
@@ -2252,6 +2318,7 @@ usageRoute.get("/", async (c) => {
|
|
|
2252
2318
|
const server = new Hono();
|
|
2253
2319
|
server.use(logger());
|
|
2254
2320
|
server.use(cors());
|
|
2321
|
+
server.use("*", createAuthMiddleware());
|
|
2255
2322
|
server.get("/", (c) => c.text("Server running"));
|
|
2256
2323
|
server.route("/chat/completions", completionRoutes);
|
|
2257
2324
|
server.route("/models", modelRoutes);
|
|
@@ -2267,4 +2334,4 @@ server.route("/v1/messages", messageRoutes);
|
|
|
2267
2334
|
|
|
2268
2335
|
//#endregion
|
|
2269
2336
|
export { server };
|
|
2270
|
-
//# sourceMappingURL=server-
|
|
2337
|
+
//# sourceMappingURL=server-DbxsVRo3.js.map
|