Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
a57c238
feature gpt-5-codex responses api
caozhiyuan Sep 26, 2025
87899a1
feat: enhance output type for function call and add content conversio…
caozhiyuan Sep 29, 2025
4fc0fa0
refactor: optimize content conversion logic in convertToolResultConte…
caozhiyuan Sep 29, 2025
2b9733b
refactor: remove unused function call output type and simplify respon…
caozhiyuan Sep 30, 2025
505f648
feat: add signature and reasoning handling to responses translation a…
caozhiyuan Sep 30, 2025
9477b45
feat: add signature to thinking messages and enhance reasoning struct…
caozhiyuan Sep 30, 2025
44551f9
refactor: remove summaryIndex from ResponsesStreamState and related h…
caozhiyuan Sep 30, 2025
708ae33
feat: enhance streaming response handling with ping mechanism
caozhiyuan Sep 30, 2025
47fb3e4
feat: responses translation add cache_read_input_tokens
caozhiyuan Oct 1, 2025
2800ed3
feat: enhance response event handling with event types and improved p…
caozhiyuan Oct 5, 2025
619d482
feat: improve event log and enhance reasoning content handling by add…
caozhiyuan Oct 7, 2025
5c6e4c6
1.fix claude code 2.0.28 warmup request consume premium request, forc…
caozhiyuan Oct 7, 2025
32cb10a
fix: the cluade code small model where max_tokens is only 512, which …
caozhiyuan Nov 3, 2025
9051a21
feat: add model reasoning efforts configuration and integrate into me…
caozhiyuan Nov 3, 2025
eeeb820
fix: ensure application directory is created when config file is missing
caozhiyuan Nov 3, 2025
3f69f13
feat: consola file logger for handler.ts
caozhiyuan Oct 29, 2025
4c0d775
fix: copolit function call returning infinite line breaks until max_t…
caozhiyuan Oct 30, 2025
1ec12db
feat: add verbose logging configuration to enhance log detail level
caozhiyuan Nov 3, 2025
174e868
fix: update verbose property to be required in State interface and ad…
caozhiyuan Nov 3, 2025
83cdfde
Merge remote-tracking branch 'remotes/origin/master' into feature/res…
caozhiyuan Nov 3, 2025
6f47926
fix: correct typo in warning message and refine whitespace handling l…
caozhiyuan Nov 6, 2025
01d4adb
fix: update token counting logic for GPT and Claude and Grok models, …
caozhiyuan Nov 10, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,11 +185,12 @@ The server exposes several endpoints to interact with the Copilot API. It provid

These endpoints mimic the OpenAI API structure.

| Endpoint | Method | Description |
| --------------------------- | ------ | --------------------------------------------------------- |
| `POST /v1/chat/completions` | `POST` | Creates a model response for the given chat conversation. |
| `GET /v1/models` | `GET` | Lists the currently available models. |
| `POST /v1/embeddings` | `POST` | Creates an embedding vector representing the input text. |
| Endpoint | Method | Description |
| --------------------------- | ------ | ---------------------------------------------------------------- |
| `POST /v1/responses` | `POST` | Most advanced interface for generating model responses. |
| `POST /v1/chat/completions` | `POST` | Creates a model response for the given chat conversation. |
| `GET /v1/models` | `GET` | Lists the currently available models. |
| `POST /v1/embeddings` | `POST` | Creates an embedding vector representing the input text. |

### Anthropic Compatible Endpoints

Expand Down
97 changes: 97 additions & 0 deletions src/lib/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import consola from "consola"
import fs from "node:fs"

import { PATHS } from "./paths"

export interface AppConfig {
extraPrompts?: Record<string, string>
smallModel?: string
modelReasoningEfforts?: Record<string, "minimal" | "low" | "medium" | "high">
}

const defaultConfig: AppConfig = {
extraPrompts: {
"gpt-5-codex": `
## Tool use
- You have access to many tools. If a tool exists to perform a specific task, you MUST use that tool instead of running a terminal command to perform that task.
### Bash tool
When using the Bash tool, follow these rules:
- always run_in_background set to false, unless you are running a long-running command (e.g., a server or a watch command).
### BashOutput tool
When using the BashOutput tool, follow these rules:
- Only Bash Tool run_in_background set to true, Use BashOutput to read the output later
### TodoWrite tool
When using the TodoWrite tool, follow these rules:
- Skip using the TodoWrite tool for tasks with three or fewer steps.
- Do not make single-step todo lists.
- When you made a todo, update it after having performed one of the sub-tasks that you shared on the todo list.
## Special user requests
- If the user makes a simple request (such as asking for the time) which you can fulfill by running a terminal command (such as 'date'), you should do so.
`,
},
smallModel: "gpt-5-mini",
modelReasoningEfforts: {
"gpt-5-mini": "low",
},
}

let cachedConfig: AppConfig | null = null

function ensureConfigFile(): void {
try {
fs.accessSync(PATHS.CONFIG_PATH, fs.constants.R_OK | fs.constants.W_OK)
} catch {
fs.mkdirSync(PATHS.APP_DIR, { recursive: true })
fs.writeFileSync(
PATHS.CONFIG_PATH,
`${JSON.stringify(defaultConfig, null, 2)}\n`,
"utf8",
)
try {
fs.chmodSync(PATHS.CONFIG_PATH, 0o600)
} catch {
return
}
}
}

function readConfigFromDisk(): AppConfig {
ensureConfigFile()
try {
const raw = fs.readFileSync(PATHS.CONFIG_PATH, "utf8")
if (!raw.trim()) {
fs.writeFileSync(
PATHS.CONFIG_PATH,
`${JSON.stringify(defaultConfig, null, 2)}\n`,
"utf8",
)
return defaultConfig
}
return JSON.parse(raw) as AppConfig
} catch (error) {
consola.error("Failed to read config file, using default config", error)
return defaultConfig
}
}

export function getConfig(): AppConfig {
cachedConfig ??= readConfigFromDisk()
return cachedConfig
}

export function getExtraPromptForModel(model: string): string {
const config = getConfig()
return config.extraPrompts?.[model] ?? ""
}

export function getSmallModel(): string {
const config = getConfig()
return config.smallModel ?? "gpt-5-mini"
}

export function getReasoningEffortForModel(
model: string,
): "minimal" | "low" | "medium" | "high" {
const config = getConfig()
return config.modelReasoningEfforts?.[model] ?? "high"
}
182 changes: 182 additions & 0 deletions src/lib/logger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
import consola, { type ConsolaInstance } from "consola"
import fs from "node:fs"
import path from "node:path"
import util from "node:util"

import { PATHS } from "./paths"
import { state } from "./state"

const LOG_RETENTION_DAYS = 7
const LOG_RETENTION_MS = LOG_RETENTION_DAYS * 24 * 60 * 60 * 1000
const CLEANUP_INTERVAL_MS = 24 * 60 * 60 * 1000
const LOG_DIR = path.join(PATHS.APP_DIR, "logs")
const FLUSH_INTERVAL_MS = 1000
const MAX_BUFFER_SIZE = 100

const logStreams = new Map<string, fs.WriteStream>()
const logBuffers = new Map<string, Array<string>>()

const ensureLogDirectory = () => {
if (!fs.existsSync(LOG_DIR)) {
fs.mkdirSync(LOG_DIR, { recursive: true })
}
}

const cleanupOldLogs = () => {
if (!fs.existsSync(LOG_DIR)) {
return
}

const now = Date.now()

for (const entry of fs.readdirSync(LOG_DIR)) {
const filePath = path.join(LOG_DIR, entry)

let stats: fs.Stats
try {
stats = fs.statSync(filePath)
} catch {
continue
}

if (!stats.isFile()) {
continue
}

if (now - stats.mtimeMs > LOG_RETENTION_MS) {
try {
fs.rmSync(filePath)
} catch {
continue
}
}
}
}

const formatArgs = (args: Array<unknown>) =>
args
.map((arg) =>
typeof arg === "string" ? arg : (
util.inspect(arg, { depth: null, colors: false })
),
)
.join(" ")

const sanitizeName = (name: string) => {
const normalized = name
.toLowerCase()
.replaceAll(/[^a-z0-9]+/g, "-")
.replaceAll(/^-+|-+$/g, "")

return normalized === "" ? "handler" : normalized
}

const getLogStream = (filePath: string): fs.WriteStream => {
let stream = logStreams.get(filePath)
if (!stream || stream.destroyed) {
stream = fs.createWriteStream(filePath, { flags: "a" })
logStreams.set(filePath, stream)

stream.on("error", (error: unknown) => {
console.warn("Log stream error", error)
logStreams.delete(filePath)
})
}
return stream
}

const flushBuffer = (filePath: string) => {
const buffer = logBuffers.get(filePath)
if (!buffer || buffer.length === 0) {
return
}

const stream = getLogStream(filePath)
const content = buffer.join("\n") + "\n"
stream.write(content, (error) => {
if (error) {
console.warn("Failed to write handler log", error)
}
})

logBuffers.set(filePath, [])
}

const flushAllBuffers = () => {
for (const filePath of logBuffers.keys()) {
flushBuffer(filePath)
}
}

const appendLine = (filePath: string, line: string) => {
let buffer = logBuffers.get(filePath)
if (!buffer) {
buffer = []
logBuffers.set(filePath, buffer)
}

buffer.push(line)

if (buffer.length >= MAX_BUFFER_SIZE) {
flushBuffer(filePath)
}
}

setInterval(flushAllBuffers, FLUSH_INTERVAL_MS)

const cleanup = () => {
flushAllBuffers()
for (const stream of logStreams.values()) {
stream.end()
}
logStreams.clear()
logBuffers.clear()
}

process.on("exit", cleanup)
process.on("SIGINT", () => {
cleanup()
process.exit(0)
})
process.on("SIGTERM", () => {
cleanup()
process.exit(0)
})

let lastCleanup = 0

export const createHandlerLogger = (name: string): ConsolaInstance => {
ensureLogDirectory()

const sanitizedName = sanitizeName(name)
const instance = consola.withTag(name)

if (state.verbose) {
instance.level = 5
}
instance.setReporters([])

instance.addReporter({
log(logObj) {
ensureLogDirectory()

if (Date.now() - lastCleanup > CLEANUP_INTERVAL_MS) {
cleanupOldLogs()
lastCleanup = Date.now()
}

const date = logObj.date
const dateKey = date.toLocaleDateString("sv-SE")
const timestamp = date.toLocaleString("sv-SE", { hour12: false })
const filePath = path.join(LOG_DIR, `${sanitizedName}-${dateKey}.log`)
const message = formatArgs(logObj.args as Array<unknown>)
const line = `[${timestamp}] [${logObj.type}] [${logObj.tag || name}]${
message ? ` ${message}` : ""
}`

appendLine(filePath, line)
},
})

return instance
}
3 changes: 3 additions & 0 deletions src/lib/paths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,18 @@ import path from "node:path"
const APP_DIR = path.join(os.homedir(), ".local", "share", "copilot-api")

const GITHUB_TOKEN_PATH = path.join(APP_DIR, "github_token")
const CONFIG_PATH = path.join(APP_DIR, "config.json")

export const PATHS = {
APP_DIR,
GITHUB_TOKEN_PATH,
CONFIG_PATH,
}

export async function ensurePaths(): Promise<void> {
await fs.mkdir(PATHS.APP_DIR, { recursive: true })
await ensureFile(PATHS.GITHUB_TOKEN_PATH)
await ensureFile(PATHS.CONFIG_PATH)
}

async function ensureFile(filePath: string): Promise<void> {
Expand Down
2 changes: 2 additions & 0 deletions src/lib/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,13 @@ export interface State {
// Rate limiting configuration
rateLimitSeconds?: number
lastRequestTimestamp?: number
verbose: boolean
}

export const state: State = {
accountType: "individual",
manualApprove: false,
rateLimitWait: false,
showToken: false,
verbose: false,
}
Loading