From 4b0dc74cbf27fc05052ac9c7c6df882691426cd2 Mon Sep 17 00:00:00 2001 From: godlockin Date: Sat, 25 Apr 2026 23:41:27 +0800 Subject: [PATCH 01/10] chore: ignore runtime artifacts (server.log/pid, .eket) --- .gitignore | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 577a4f199..605d69266 100644 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,10 @@ node_modules/ .eslintcache # build output -dist/ \ No newline at end of file +dist/ + +# runtime artifacts +server.log +server.pid +.eket/ +.eket-*.log \ No newline at end of file From eda5a6a54ebdaecfd6006a01cea7aee84c973a18 Mon Sep 17 00:00:00 2001 From: godlockin Date: Sat, 25 Apr 2026 23:41:58 +0800 Subject: [PATCH 02/10] chore: rebrand fork as godlockin/copilot-api v0.7.4 --- bun.lock | 3 ++- package.json | 19 ++++++++++--------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/bun.lock b/bun.lock index 20e895e7f..64ce2bfd4 100644 --- a/bun.lock +++ b/bun.lock @@ -1,8 +1,9 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { - "name": "copilot-api", + "name": "betahi-copilot-api", "dependencies": { "citty": "^0.1.6", "clipboardy": "^5.0.0", diff --git a/package.json b/package.json index a5adbb8e7..a5659fcc9 100644 --- a/package.json +++ b/package.json @@ -1,19 +1,19 @@ { - "name": "copilot-api", - "version": "0.7.0", - "description": "Turn GitHub Copilot into OpenAI/Anthropic API compatible server. Usable with Claude Code!", + "name": "betahi-copilot-api", + "version": "0.7.4", + "description": "GitHub Copilot proxy with OpenAI/Anthropic compatibility, GPT-5 and Gemini support for Claude Code.", "keywords": [ "proxy", "github-copilot", "openai-compatible" ], - "homepage": "https://github.com/ericc-ch/copilot-api", - "bugs": "https://github.com/ericc-ch/copilot-api/issues", + "homepage": "https://github.com/betaHi/copilot-api", + "bugs": "https://github.com/betaHi/copilot-api/issues", "repository": { "type": "git", - "url": "git+https://github.com/ericc-ch/copilot-api.git" + "url": "git+https://github.com/betaHi/copilot-api.git" }, - "author": "Erick Christian ", + "author": "betaHi", "type": "module", "bin": { "copilot-api": "./dist/main.js" @@ -25,8 +25,8 @@ "build": "tsdown", "dev": "bun run --watch ./src/main.ts", "knip": "knip-bun", - "lint": "eslint --cache", - "lint:all": "eslint --cache .", + "lint": "eslint --cache --no-warn-ignored", + "lint:all": "eslint --cache --no-warn-ignored .", "prepack": "bun run build", "prepare": "simple-git-hooks", "release": "bumpp && bun publish --access public", @@ -54,6 +54,7 @@ }, "devDependencies": { "@echristian/eslint-config": "^0.0.54", + "@eslint/markdown": "^8.0.1", "@types/bun": "^1.2.23", "@types/proxy-from-env": "^1.0.4", "bumpp": "^10.2.3", From ce0f0cbf8af1de3d6eac97770d90720894826dec Mon Sep 17 00:00:00 2001 From: godlockin Date: Sat, 25 Apr 2026 23:42:23 +0800 Subject: [PATCH 03/10] feat: add model resolution, claude settings, and Responses API support - src/lib/models.ts: resolveModel/resolveModelId with snapshot suffix stripping (e.g. claude-opus-4-7-{snapshot} -> claude-opus-4.7) and family alias matching - src/lib/claude-settings.ts: read env from ~/.claude/settings*.json for COPILOT_REASONING_EFFORT overrides - src/services/copilot/responses.ts: GitHub Copilot /responses endpoint adapter for gpt-5.3-codex / gpt-5.4-mini (uses reasoning.effort, not budget_tokens) - routes: thread resolveModel through chat-completions and count-tokens handlers - anthropic-types: extend thinking.type with "adaptive" and add reasoning_effort --- src/lib/claude-settings.ts | 56 ++ src/lib/models.ts | 150 +++++ src/routes/chat-completions/handler.ts | 16 +- src/routes/messages/anthropic-types.ts | 3 +- src/routes/messages/count-tokens-handler.ts | 15 +- src/services/copilot/responses.ts | 583 ++++++++++++++++++++ 6 files changed, 811 insertions(+), 12 deletions(-) create mode 100644 src/lib/claude-settings.ts create mode 100644 src/lib/models.ts create mode 100644 src/services/copilot/responses.ts diff --git a/src/lib/claude-settings.ts b/src/lib/claude-settings.ts new file mode 100644 index 000000000..d2a7a2212 --- /dev/null +++ b/src/lib/claude-settings.ts @@ -0,0 +1,56 @@ +import consola from "consola" +import fs from "node:fs/promises" +import os from "node:os" +import path from "node:path" + +interface ClaudeSettingsFile { + env?: Record +} + +const getClaudeSettingsPaths = (): Array => { + const currentWorkingDirectory = process.cwd() + const homeDirectory = process.env.HOME ?? os.homedir() + + return [ + path.join(homeDirectory, ".claude", "settings.json"), + path.join(currentWorkingDirectory, ".claude", "settings.json"), + path.join(currentWorkingDirectory, ".claude", "settings.local.json"), + ] +} + +const readClaudeSettingsFile = async ( + filePath: string, +): Promise => { + try { + const content = await fs.readFile(filePath) + return JSON.parse(content) as ClaudeSettingsFile + } catch (error) { + if ((error as NodeJS.ErrnoException).code === "ENOENT") { + return undefined + } + + consola.warn(`Failed to read Claude settings from ${filePath}:`, error) + return undefined + } +} + +export const getClaudeSettingsEnv = async (): Promise< + Record +> => { + const mergedEnv: Record = {} + + for (const filePath of getClaudeSettingsPaths()) { + const settings = await readClaudeSettingsFile(filePath) + if (!settings?.env) { + continue + } + + for (const [key, value] of Object.entries(settings.env)) { + if (typeof value === "string") { + mergedEnv[key] = value + } + } + } + + return mergedEnv +} diff --git a/src/lib/models.ts b/src/lib/models.ts new file mode 100644 index 000000000..e976e5b4b --- /dev/null +++ b/src/lib/models.ts @@ -0,0 +1,150 @@ +import type { Model } from "~/services/copilot/get-models" + +import { state } from "./state" + +const normalizeModelId = (modelId: string): string => + modelId + .trim() + .toLowerCase() + .replaceAll(/[\s._-]+/g, "") + +const stripSnapshotSuffix = (modelId: string): string => { + if (modelId.startsWith("claude-sonnet-4-")) { + return "claude-sonnet-4" + } + + if (/^claude-opus-4-7-\d{8}$/.test(modelId)) { + return "claude-opus-4.7" + } + + if (modelId.startsWith("claude-opus-4-")) { + return "claude-opus-4" + } + + return modelId +} + +const getAliasCandidates = (modelId: string): Array => { + const canonicalModelId = stripSnapshotSuffix(modelId.trim().toLowerCase()) + const aliases = new Set([canonicalModelId]) + + const familyMatch = canonicalModelId.match( + /^[a-z]+(?:-[a-z]+)*-\d+(?:\.\d+)?/, + ) + if (familyMatch) { + aliases.add(familyMatch[0]) + aliases.add(familyMatch[0].replace(/\.\d+$/, "")) + } + + if (/^gpt-5(?:[.-]\d+)?$/i.test(canonicalModelId)) { + aliases.add("gpt-5") + } + + return [...aliases] +} + +const scoreModelCandidate = (model: Model): number => { + let score = 0 + + if (model.model_picker_enabled) { + score += 20 + } + + if (!model.preview) { + score += 10 + } + + if (/mini|nano|fast|flash|haiku/i.test(model.id)) { + score -= 15 + } + + return score - model.id.length / 1000 +} + +const pickBestModel = (models: Array): Model | undefined => { + return [...models].sort( + (left, right) => scoreModelCandidate(right) - scoreModelCandidate(left), + )[0] +} + +const getBestPrefixMatches = ( + models: Array, + aliasCandidates: Array, +): Array => { + let bestMatchLength = 0 + const matches: Array = [] + + for (const model of models) { + const normalizedModelId = normalizeModelId(model.id) + const matchedAliasLength = Math.max( + 0, + ...aliasCandidates.map((candidate) => { + const normalizedCandidate = normalizeModelId(candidate) + + return ( + normalizedCandidate.length >= 4 + && normalizedModelId.startsWith(normalizedCandidate) + ) ? + normalizedCandidate.length + : 0 + }), + ) + + if (matchedAliasLength === 0) { + continue + } + + if (matchedAliasLength > bestMatchLength) { + bestMatchLength = matchedAliasLength + matches.length = 0 + matches.push(model) + continue + } + + if (matchedAliasLength === bestMatchLength) { + matches.push(model) + } + } + + return matches +} + +export const resolveModel = ( + requestedModelId: string, + models: Array | undefined = state.models?.data, +): Model | undefined => { + if (!models || models.length === 0) { + return undefined + } + + const exactMatch = models.find((model) => model.id === requestedModelId) + if (exactMatch) { + return exactMatch + } + + const aliasCandidates = getAliasCandidates(requestedModelId) + const normalizedAliases = new Set( + aliasCandidates.map((candidate) => normalizeModelId(candidate)), + ) + + const normalizedExactMatches = models.filter((model) => + normalizedAliases.has(normalizeModelId(model.id)), + ) + if (normalizedExactMatches.length > 0) { + return pickBestModel(normalizedExactMatches) + } + + const familyPrefixMatches = getBestPrefixMatches(models, aliasCandidates) + + return pickBestModel(familyPrefixMatches) +} + +export const resolveModelId = ( + requestedModelId: string, + models: Array | undefined = state.models?.data, +): string => + resolveModel(requestedModelId, models)?.id + ?? stripSnapshotSuffix(requestedModelId) + +export const isClaudeOpus47Model = (modelId: string): boolean => + modelId === "claude-opus-4.7" diff --git a/src/routes/chat-completions/handler.ts b/src/routes/chat-completions/handler.ts index 04a5ae9ed..6d7539e6c 100644 --- a/src/routes/chat-completions/handler.ts +++ b/src/routes/chat-completions/handler.ts @@ -4,6 +4,7 @@ import consola from "consola" import { streamSSE, type SSEMessage } from "hono/streaming" import { awaitApproval } from "~/lib/approval" +import { resolveModel } from "~/lib/models" import { checkRateLimit } from "~/lib/rate-limit" import { state } from "~/lib/state" import { getTokenCount } from "~/lib/tokenizer" @@ -20,10 +21,19 @@ export async function handleCompletion(c: Context) { let payload = await c.req.json() consola.debug("Request payload:", JSON.stringify(payload).slice(-400)) + const resolvedModel = resolveModel(payload.model) + if (resolvedModel && resolvedModel.id !== payload.model) { + consola.info(`Resolved model alias ${payload.model} -> ${resolvedModel.id}`) + payload = { + ...payload, + model: resolvedModel.id, + } + } + // Find the selected model - const selectedModel = state.models?.data.find( - (model) => model.id === payload.model, - ) + const selectedModel = + resolvedModel + ?? state.models?.data.find((model) => model.id === payload.model) // Calculate and display token count try { diff --git a/src/routes/messages/anthropic-types.ts b/src/routes/messages/anthropic-types.ts index 881fffcc8..fefbcdd49 100644 --- a/src/routes/messages/anthropic-types.ts +++ b/src/routes/messages/anthropic-types.ts @@ -19,9 +19,10 @@ export interface AnthropicMessagesPayload { name?: string } thinking?: { - type: "enabled" + type: "enabled" | "adaptive" budget_tokens?: number } + reasoning_effort?: "none" | "low" | "medium" | "high" | "max" | "xhigh" service_tier?: "auto" | "standard_only" } diff --git a/src/routes/messages/count-tokens-handler.ts b/src/routes/messages/count-tokens-handler.ts index 2ec849cb8..98df2a54f 100644 --- a/src/routes/messages/count-tokens-handler.ts +++ b/src/routes/messages/count-tokens-handler.ts @@ -2,7 +2,7 @@ import type { Context } from "hono" import consola from "consola" -import { state } from "~/lib/state" +import { resolveModel } from "~/lib/models" import { getTokenCount } from "~/lib/tokenizer" import { type AnthropicMessagesPayload } from "./anthropic-types" @@ -19,9 +19,7 @@ export async function handleCountTokens(c: Context) { const openAIPayload = translateToOpenAI(anthropicPayload) - const selectedModel = state.models?.data.find( - (model) => model.id === anthropicPayload.model, - ) + const selectedModel = resolveModel(anthropicPayload.model) if (!selectedModel) { consola.warn("Model not found, returning default token count") @@ -31,6 +29,7 @@ export async function handleCountTokens(c: Context) { } const tokenCount = await getTokenCount(openAIPayload, selectedModel) + const effectiveModelId = selectedModel.id if (anthropicPayload.tools && anthropicPayload.tools.length > 0) { let mcpToolExist = false @@ -40,19 +39,19 @@ export async function handleCountTokens(c: Context) { ) } if (!mcpToolExist) { - if (anthropicPayload.model.startsWith("claude")) { + if (effectiveModelId.startsWith("claude")) { // https://docs.anthropic.com/en/docs/agents-and-tools/tool-use/overview#pricing tokenCount.input = tokenCount.input + 346 - } else if (anthropicPayload.model.startsWith("grok")) { + } else if (effectiveModelId.startsWith("grok")) { tokenCount.input = tokenCount.input + 480 } } } let finalTokenCount = tokenCount.input + tokenCount.output - if (anthropicPayload.model.startsWith("claude")) { + if (effectiveModelId.startsWith("claude")) { finalTokenCount = Math.round(finalTokenCount * 1.15) - } else if (anthropicPayload.model.startsWith("grok")) { + } else if (effectiveModelId.startsWith("grok")) { finalTokenCount = Math.round(finalTokenCount * 1.03) } diff --git a/src/services/copilot/responses.ts b/src/services/copilot/responses.ts new file mode 100644 index 000000000..2db4bbc63 --- /dev/null +++ b/src/services/copilot/responses.ts @@ -0,0 +1,583 @@ +import { randomUUID } from "node:crypto" + +import type { + ChatCompletionChunk, + ChatCompletionResponse, + ChatCompletionsPayload, + ContentPart, + Message, + Tool, +} from "./create-chat-completions" + +import { sanitizeUserIdentifier } from "./create-chat-completions" + +export interface ResponseStreamEventMessage { + data?: string + event?: string +} + +export interface ResponsesApiResponse { + id: string + created_at: number + model: string + output: Array + usage?: { + input_tokens: number + input_tokens_details?: { + cached_tokens?: number + } + output_tokens: number + output_tokens_details?: { + reasoning_tokens?: number + } + total_tokens: number + } + incomplete_details?: { + reason?: string | null + } | null +} + +export type ResponsesReasoningEffort = + | "none" + | "low" + | "medium" + | "high" + | "max" + | "xhigh" + +type ResponsesInput = string | Array + +type ResponsesInputItem = + | ResponsesMessageInput + | ResponsesFunctionCallInput + | ResponsesFunctionCallOutputInput + +type ResponsesMessageInput = { + role: "user" | "assistant" | "system" | "developer" + content: string | Array +} + +type ResponsesInputContentPart = + | { + type: "input_text" + text: string + } + | { + type: "input_image" + image_url: string + detail: "low" | "high" | "auto" + } + +type ResponsesFunctionCallInput = { + type: "function_call" + call_id: string + name: string + arguments: string +} + +type ResponsesFunctionCallOutputInput = { + type: "function_call_output" + call_id: string + output: string +} + +type ResponsesToolChoice = + | "none" + | "auto" + | "required" + | { type: "function"; name: string } + +export interface ResponsesRequestPayload { + model: string + input: ResponsesInput + stream?: boolean | null + max_output_tokens?: number | null + temperature?: number | null + top_p?: number | null + user?: string | null + reasoning?: { + effort: ResponsesReasoningEffort + } + tools?: Array + tool_choice?: ResponsesToolChoice | null + text?: { + format: { + type: "json_object" + } + } +} + +interface ResponsesTool { + type: "function" + name: string + description?: string + parameters: Record +} + +type ResponsesOutputItem = + | ResponsesMessageOutputItem + | ResponsesFunctionCallOutputItem + | ResponsesReasoningOutputItem + +interface ResponsesMessageOutputItem { + type: "message" + role: "assistant" + content: Array +} + +interface ResponsesMessageContentPart { + type: "output_text" + text: string +} + +interface ResponsesFunctionCallOutputItem { + type: "function_call" + call_id: string + name: string + arguments: string +} + +interface ResponsesReasoningOutputItem { + type: "reasoning" +} + +interface ResponsesStreamEnvelope { + type: string + response?: { + id: string + created_at: number + model: string + usage?: ResponsesApiResponse["usage"] + output?: Array + incomplete_details?: ResponsesApiResponse["incomplete_details"] + } + item?: + | Partial + | Partial + output_index?: number + delta?: string +} + +interface ResponseTranslationState { + responseId: string + createdAt: number + model: string + started: boolean +} + +interface CreateChunkOptions { + delta: ChatCompletionChunk["choices"][0]["delta"] + finishReason?: ChatCompletionChunk["choices"][0]["finish_reason"] + usage?: ChatCompletionChunk["usage"] +} + +const RESPONSES_ONLY_MODEL_PATTERN = /^(?:gpt-5\.3-codex|gpt-5\.4-mini)(?:-|$)/i + +export function shouldUseResponsesApiForModel(model: string): boolean { + return RESPONSES_ONLY_MODEL_PATTERN.test(model) +} + +export function buildResponsesRequestPayload( + payload: ChatCompletionsPayload, + reasoningEffort: ResponsesReasoningEffort | undefined, +): ResponsesRequestPayload { + return { + model: payload.model, + input: translateMessagesToResponsesInput(payload.messages), + stream: payload.stream, + max_output_tokens: payload.max_tokens, + temperature: payload.temperature, + top_p: payload.top_p, + user: sanitizeUserIdentifier(payload.user), + tools: translateTools(payload.tools), + tool_choice: translateToolChoice(payload.tool_choice), + reasoning: reasoningEffort ? { effort: reasoningEffort } : undefined, + text: + payload.response_format?.type === "json_object" ? + { format: { type: "json_object" } } + : undefined, + } +} + +export function translateResponsesToChatCompletion( + response: ResponsesApiResponse, +): ChatCompletionResponse { + const assistantMessages = response.output.filter( + (item): item is ResponsesMessageOutputItem => item.type === "message", + ) + const functionCalls = response.output.filter( + (item): item is ResponsesFunctionCallOutputItem => + item.type === "function_call", + ) + + const content = assistantMessages + .flatMap((item) => item.content) + .map((part) => part.text) + .join("") + + return { + id: response.id, + object: "chat.completion", + created: response.created_at, + model: response.model, + choices: [ + { + index: 0, + message: { + role: "assistant", + content: content || null, + ...(functionCalls.length > 0 && { + tool_calls: functionCalls.map((toolCall) => ({ + id: toolCall.call_id, + type: "function" as const, + function: { + name: toolCall.name, + arguments: toolCall.arguments, + }, + })), + }), + }, + logprobs: null, + finish_reason: getFinishReason(response, functionCalls.length > 0), + }, + ], + usage: translateUsage(response.usage), + } +} + +export async function* translateResponsesStreamToChatCompletionStream( + responseStream: AsyncIterable, +): AsyncGenerator { + const state: ResponseTranslationState = { + responseId: randomUUID(), + createdAt: Math.floor(Date.now() / 1000), + model: "", + started: false, + } + + for await (const rawEvent of responseStream) { + if (!rawEvent.data || rawEvent.data === "[DONE]") { + continue + } + + const event = JSON.parse(rawEvent.data) as ResponsesStreamEnvelope + + if (event.response) { + state.responseId = event.response.id + state.createdAt = event.response.created_at + state.model = event.response.model + } + + if (event.type === "response.output_item.added") { + const chunk = handleOutputItemAdded(state, event) + if (chunk) { + yield chunk + } + continue + } + + if (event.type === "response.output_text.delta") { + yield createRoleChunk(state) + yield createChunk(state, { delta: { content: event.delta } }) + continue + } + + if (event.type === "response.function_call_arguments.delta") { + if (event.output_index === undefined) { + continue + } + + yield createRoleChunk(state) + yield createChunk(state, { + delta: { + tool_calls: [ + { + index: event.output_index, + type: "function", + function: { + arguments: event.delta ?? "", + }, + }, + ], + }, + }) + continue + } + + if (event.type === "response.completed") { + if (!event.response) { + continue + } + + yield createChunk( + { + ...state, + responseId: event.response.id, + createdAt: event.response.created_at, + model: event.response.model, + }, + { + delta: {}, + finishReason: getFinishReason( + { + output: event.response.output ?? [], + incomplete_details: event.response.incomplete_details, + }, + (event.response.output ?? []).some( + (item) => item.type === "function_call", + ), + ), + usage: translateUsage(event.response.usage), + }, + ) + yield { data: "[DONE]" } + return + } + } +} + +function handleOutputItemAdded( + state: ResponseTranslationState, + event: ResponsesStreamEnvelope, +): ResponseStreamEventMessage | undefined { + if (event.item?.type === "message") { + return createRoleChunk(state) + } + + if ( + event.item?.type === "function_call" + && event.output_index !== undefined + ) { + state.started = true + return createChunk(state, { + delta: { + role: "assistant", + tool_calls: [ + { + index: event.output_index, + id: event.item.call_id, + type: "function", + function: { + name: event.item.name, + arguments: event.item.arguments ?? "", + }, + }, + ], + }, + }) + } + + return undefined +} + +function createRoleChunk( + state: ResponseTranslationState, +): ResponseStreamEventMessage { + if (state.started) { + return { data: "" } + } + + state.started = true + return createChunk(state, { delta: { role: "assistant" } }) +} + +function createChunk( + state: ResponseTranslationState, + { delta, finishReason = null, usage }: CreateChunkOptions, +): ResponseStreamEventMessage { + return { + data: JSON.stringify({ + id: state.responseId, + object: "chat.completion.chunk", + created: state.createdAt, + model: state.model, + choices: [ + { + index: 0, + delta, + finish_reason: finishReason, + logprobs: null, + }, + ], + usage, + } satisfies ChatCompletionChunk), + } +} + +function translateMessagesToResponsesInput( + messages: Array, +): ResponsesInput { + const items = messages.flatMap((message) => translateMessage(message)) + + if ( + items.length === 1 + && "role" in items[0] + && items[0].role === "user" + && typeof items[0].content === "string" + ) { + return items[0].content + } + + return items +} + +function translateMessage(message: Message): Array { + if (message.role === "tool") { + return [ + { + type: "function_call_output", + call_id: message.tool_call_id ?? "", + output: stringifyToolOutput(message.content), + }, + ] + } + + const translated: Array = [] + + if (hasContent(message.content)) { + translated.push({ + role: message.role, + content: translateContent(message.content), + }) + } + + if (message.role === "assistant" && message.tool_calls) { + translated.push( + ...message.tool_calls.map((toolCall) => ({ + type: "function_call" as const, + call_id: toolCall.id, + name: toolCall.function.name, + arguments: toolCall.function.arguments, + })), + ) + } + + return translated +} + +function hasContent(content: Message["content"]): boolean { + if (content === null) { + return false + } + + if (typeof content === "string") { + return content.length > 0 + } + + return content.length > 0 +} + +function translateContent( + content: Message["content"], +): ResponsesMessageInput["content"] { + if (typeof content === "string") { + return content + } + + if (!content || content.length === 0) { + return "" + } + + return content.map((part) => translateContentPart(part)) +} + +function translateContentPart(part: ContentPart): ResponsesInputContentPart { + if (part.type === "text") { + return { + type: "input_text", + text: part.text, + } + } + + return { + type: "input_image", + image_url: part.image_url.url, + detail: part.image_url.detail ?? "auto", + } +} + +function stringifyToolOutput(content: Message["content"]): string { + if (typeof content === "string") { + return content + } + + if (!content) { + return "" + } + + const text = content + .filter( + (part): part is Extract => + part.type === "text", + ) + .map((part) => part.text) + .join("\n\n") + + return text || JSON.stringify(content) +} + +function translateTools( + tools: Array | null | undefined, +): Array | undefined { + if (!tools) { + return undefined + } + + return tools.map((tool) => ({ + type: "function", + name: tool.function.name, + description: tool.function.description, + parameters: tool.function.parameters, + })) +} + +function translateToolChoice( + toolChoice: ChatCompletionsPayload["tool_choice"], +): ResponsesToolChoice | undefined { + if (!toolChoice) { + return undefined + } + + if (typeof toolChoice === "string") { + return toolChoice + } + + return { + type: "function", + name: toolChoice.function.name, + } +} + +function translateUsage( + usage: ResponsesApiResponse["usage"] | undefined, +): ChatCompletionResponse["usage"] | undefined { + if (!usage) { + return undefined + } + + return { + prompt_tokens: usage.input_tokens, + completion_tokens: usage.output_tokens, + total_tokens: usage.total_tokens, + ...(usage.input_tokens_details?.cached_tokens !== undefined && { + prompt_tokens_details: { + cached_tokens: usage.input_tokens_details.cached_tokens, + }, + }), + } +} + +function getFinishReason( + response: Pick, + hasFunctionCalls: boolean, +): "stop" | "length" | "tool_calls" | "content_filter" { + if (hasFunctionCalls) { + return "tool_calls" + } + + if (response.incomplete_details?.reason?.includes("max_output_tokens")) { + return "length" + } + + return "stop" +} From 29dfb09baf86b9fee626fb79945d696bfa89893d Mon Sep 17 00:00:00 2001 From: godlockin Date: Sat, 25 Apr 2026 23:42:57 +0800 Subject: [PATCH 04/10] fix: extend network timeouts to survive long subagent workloads MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - main.ts: install global undici Agent with 10min headers/body timeout, 2min keep-alive, 30s connect — default 300s headersTimeout was killing >5min Copilot upstream responses (DOMException TIMEOUT_ERR) - start.ts: pass bun idleTimeout=255 (Bun max) to srvx + reusePort=true so keep-alive connections aren't closed under 10s default, which surfaces as "socket connection was closed unexpectedly" on the Claude Code side --- src/main.ts | 13 +++++++++++++ src/start.ts | 9 ++++++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/main.ts b/src/main.ts index 4f6ca784b..d6cd9e3dc 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,12 +1,25 @@ #!/usr/bin/env node import { defineCommand, runMain } from "citty" +import { Agent, setGlobalDispatcher } from "undici" import { auth } from "./auth" import { checkUsage } from "./check-usage" import { debug } from "./debug" import { start } from "./start" +// Extend undici global timeouts to tolerate long GitHub Copilot upstream responses +// (subagent workloads routinely exceed default 300s headersTimeout). +setGlobalDispatcher( + new Agent({ + headersTimeout: 600_000, + bodyTimeout: 600_000, + keepAliveTimeout: 120_000, + keepAliveMaxTimeout: 600_000, + connect: { timeout: 30_000 }, + }), +) + const main = defineCommand({ meta: { name: "copilot-api", diff --git a/src/start.ts b/src/start.ts index 14abbbdff..d5a05d0c7 100644 --- a/src/start.ts +++ b/src/start.ts @@ -117,7 +117,14 @@ export async function runServer(options: RunServerOptions): Promise { serve({ fetch: server.fetch as ServerHandler, port: options.port, - }) + reusePort: true, + bun: { + // Bun default is 10s; cap at 255 (Bun max) so subagent long-poll / keep-alive + // connections aren't killed mid-stream, which surfaces as + // "socket connection was closed unexpectedly" on the Claude Code side. + idleTimeout: 255, + }, + } as Parameters[0]) } export const start = defineCommand({ From 03862dab42bbe0d26470e3709141b1d9976d51b5 Mon Sep 17 00:00:00 2001 From: godlockin Date: Sat, 25 Apr 2026 23:47:13 +0800 Subject: [PATCH 05/10] fix(opus-4.7): pass thinking.enabled verbatim and cap effort at medium Upstream Copilot opus-4.7 only accepts {type: "enabled"} (no "adaptive", no "disabled"), and rejects effort levels above "medium". Coerce both "enabled" and "adaptive" to "enabled" with optional budget_tokens, and downgrade higher efforts to "medium" with a warning so legacy clients keep working. Co-Authored-By: Claude Opus 4 --- src/routes/messages/non-stream-translation.ts | 217 +++++++++++- .../copilot/create-chat-completions.ts | 317 +++++++++++++++++- tests/anthropic-request.test.ts | 118 +++++++ 3 files changed, 644 insertions(+), 8 deletions(-) diff --git a/src/routes/messages/non-stream-translation.ts b/src/routes/messages/non-stream-translation.ts index dc41e6382..62dd79fd2 100644 --- a/src/routes/messages/non-stream-translation.ts +++ b/src/routes/messages/non-stream-translation.ts @@ -1,4 +1,8 @@ +import consola from "consola" + +import { isClaudeOpus47Model, resolveModelId } from "~/lib/models" import { + sanitizeReasoningEffortForModel, type ChatCompletionResponse, type ChatCompletionsPayload, type ContentPart, @@ -40,20 +44,219 @@ export function translateToOpenAI( stream: payload.stream, temperature: payload.temperature, top_p: payload.top_p, + thinking: translateThinking(payload), + output_config: translateOutputConfig(payload), + reasoning_effort: translateReasoningEffort(payload), user: payload.metadata?.user_id, tools: translateAnthropicToolsToOpenAI(payload.tools), tool_choice: translateAnthropicToolChoiceToOpenAI(payload.tool_choice), } } -function translateModelName(model: string): string { - // Subagent requests use a specific model number which Copilot doesn't support - if (model.startsWith("claude-sonnet-4-")) { - return model.replace(/^claude-sonnet-4-.*/, "claude-sonnet-4") - } else if (model.startsWith("claude-opus-")) { - return model.replace(/^claude-opus-4-.*/, "claude-opus-4") +function isClaudeModel(modelId: string): boolean { + return modelId.startsWith("claude-") +} + +type ClaudeOpus47Effort = NonNullable< + NonNullable["effort"] +> + +// Per-model effort caps. Upstream rejects efforts above the model's tier. +// TODO: keep in sync with the mirror in services/copilot/create-chat-completions.ts. +const OPUS_47_ALLOWED_EFFORTS: Array = ["low", "medium"] + +function getAllowedClaudeEfforts(modelId: string): Array { + if (isClaudeOpus47Model(modelId)) { + return OPUS_47_ALLOWED_EFFORTS + } + return [] +} + +function capClaudeEffort( + modelId: string, + effort: ClaudeOpus47Effort | undefined, +): ClaudeOpus47Effort | undefined { + if (!effort) { + return undefined + } + const allowed = getAllowedClaudeEfforts(modelId) + if (allowed.length === 0) { + return effort + } + if (allowed.includes(effort)) { + return effort + } + const capped = allowed.at(-1) + consola.warn( + `[${modelId}] effort "${effort}" exceeds cap; downgraded to "${capped}"`, + ) + return capped +} + +function normalizeClaudeEffort( + value: string | undefined, +): ClaudeOpus47Effort | undefined { + switch (value?.toLowerCase()) { + case "low": { + return "low" + } + case "medium": { + return "medium" + } + case "high": { + return "high" + } + case "xhigh": { + return "xhigh" + } + case "max": { + return "max" + } + default: { + return undefined + } } - return model +} + +function getClaudeOpus47Effort( + payload: AnthropicMessagesPayload, +): ClaudeOpus47Effort | undefined { + const explicitEffort = normalizeClaudeEffort(payload.reasoning_effort) + if (explicitEffort) { + return explicitEffort + } + + if (payload.thinking?.type !== "enabled") { + return undefined + } + + const budgetTokens = payload.thinking.budget_tokens + if (budgetTokens === undefined) { + return "medium" + } + + if (budgetTokens <= 2_048) { + return "low" + } + + if (budgetTokens <= 8_192) { + return "medium" + } + + if (budgetTokens <= 24_576) { + return "high" + } + + return "xhigh" +} + +function translateThinking( + payload: AnthropicMessagesPayload, +): ChatCompletionsPayload["thinking"] { + const modelId = translateModelName(payload.model) + + if (!isClaudeOpus47Model(modelId)) { + return undefined + } + + const t = payload.thinking + if (!t) { + return undefined + } + + // Upstream Copilot opus-4.7 only accepts {type: "enabled"} (no "adaptive", + // no "disabled"). The Anthropic schema admits "enabled" | "adaptive"; we + // coerce both to "enabled" so legacy clients sending "adaptive" keep working. + return t.budget_tokens === undefined ? + { type: "enabled" } + : { type: "enabled", budget_tokens: t.budget_tokens } +} + +function translateOutputConfig( + payload: AnthropicMessagesPayload, +): ChatCompletionsPayload["output_config"] { + const modelId = translateModelName(payload.model) + + if (!isClaudeOpus47Model(modelId)) { + return undefined + } + + const raw = getClaudeOpus47Effort(payload) + const capped = capClaudeEffort(modelId, raw) + + return capped ? { effort: capped } : undefined +} + +function translateReasoningEffort( + payload: AnthropicMessagesPayload, +): ChatCompletionsPayload["reasoning_effort"] { + const modelId = translateModelName(payload.model) + + if (isClaudeModel(modelId)) { + return undefined + } + + if (payload.reasoning_effort) { + return sanitizeReasoningEffortForModel( + modelId, + normalizeReasoningEffort(payload.reasoning_effort), + ) + } + + if (payload.thinking?.type !== "enabled") { + return undefined + } + + const budgetTokens = payload.thinking.budget_tokens + if (budgetTokens === undefined) { + return "medium" + } + + if (budgetTokens <= 2_048) { + return "low" + } + + if (budgetTokens <= 8_192) { + return "medium" + } + + if (budgetTokens <= 24_576) { + return sanitizeReasoningEffortForModel(modelId, "high") + } + + return sanitizeReasoningEffortForModel(modelId, "xhigh") +} + +function normalizeReasoningEffort( + value: string, +): ChatCompletionsPayload["reasoning_effort"] { + switch (value.toLowerCase()) { + case "none": { + return "none" + } + case "low": { + return "low" + } + case "medium": { + return "medium" + } + case "high": { + return "high" + } + case "xhigh": { + return "xhigh" + } + case "max": { + return "max" + } + default: { + return undefined + } + } +} + +function translateModelName(model: string): string { + return resolveModelId(model) } function translateAnthropicMessagesToOpenAI( diff --git a/src/services/copilot/create-chat-completions.ts b/src/services/copilot/create-chat-completions.ts index 8534151da..9840d4e27 100644 --- a/src/services/copilot/create-chat-completions.ts +++ b/src/services/copilot/create-chat-completions.ts @@ -2,9 +2,251 @@ import consola from "consola" import { events } from "fetch-event-stream" import { copilotHeaders, copilotBaseUrl } from "~/lib/api-config" +import { getClaudeSettingsEnv } from "~/lib/claude-settings" import { HTTPError } from "~/lib/error" +import { isClaudeOpus47Model } from "~/lib/models" import { state } from "~/lib/state" +import { + buildResponsesRequestPayload, + shouldUseResponsesApiForModel, + translateResponsesStreamToChatCompletionStream, + translateResponsesToChatCompletion, + type ResponsesApiResponse, + type ResponsesReasoningEffort, +} from "./responses" + +const usesMaxCompletionTokens = (modelId: string): boolean => + modelId.startsWith("gpt-5") + +type ClaudeOpus47Effort = NonNullable< + NonNullable["effort"] +> + +// Per-model effort caps for Claude. Mirror of the helper in +// routes/messages/non-stream-translation.ts — keep in sync. +const OPUS_47_ALLOWED_EFFORTS: Array = ["low", "medium"] + +export const getAllowedClaudeEfforts = ( + modelId: string, +): Array => + isClaudeOpus47Model(modelId) ? OPUS_47_ALLOWED_EFFORTS : [] + +const sanitizeClaudeEffortForModel = ( + modelId: string, + effort: ClaudeOpus47Effort | undefined, +): ClaudeOpus47Effort | undefined => { + if (!effort) { + return undefined + } + const allowed = getAllowedClaudeEfforts(modelId) + if (allowed.length === 0) { + return effort + } + if (allowed.includes(effort)) { + return effort + } + const capped = allowed.at(-1) + consola.warn( + `[${modelId}] effort "${effort}" exceeds cap; downgraded to "${capped}"`, + ) + return capped +} + +// Copilot rejects user identifiers longer than 64 characters. +const MAX_USER_LENGTH = 64 + +const defaultReasoningEffort = ( + modelId: string, +): ChatCompletionsPayload["reasoning_effort"] => + usesMaxCompletionTokens(modelId) ? "medium" : undefined + +const getAllowedReasoningEfforts = ( + modelId: string, +): Array< + Exclude +> => { + if (modelId.startsWith("gpt-5.4-mini")) { + return ["none", "low", "medium"] + } + + if (modelId.startsWith("gpt-5.4") || modelId.startsWith("gpt-5.3-codex")) { + return ["low", "medium", "high", "xhigh"] + } + + if (usesMaxCompletionTokens(modelId)) { + return ["low", "medium", "high", "xhigh"] + } + + return [] +} + +export const sanitizeReasoningEffortForModel = ( + modelId: string, + reasoningEffort: ChatCompletionsPayload["reasoning_effort"], +): ChatCompletionsPayload["reasoning_effort"] => { + if (!reasoningEffort) { + return undefined + } + + return getAllowedReasoningEfforts(modelId).includes(reasoningEffort) ? + reasoningEffort + : undefined +} + +const getRequestedReasoningEffort = ( + payload: ChatCompletionsPayload, + claudeSettingsEnv: Record, +): ChatCompletionsPayload["reasoning_effort"] => { + const requestedReasoningEffort = + payload.reasoning_effort + ?? normalizeReasoningEffort(process.env.COPILOT_REASONING_EFFORT) + ?? normalizeReasoningEffort(claudeSettingsEnv.COPILOT_REASONING_EFFORT) + + return ( + sanitizeReasoningEffortForModel(payload.model, requestedReasoningEffort) + ?? defaultReasoningEffort(payload.model) + ) +} + +const normalizeReasoningEffort = ( + value: string | undefined | null, +): ChatCompletionsPayload["reasoning_effort"] => { + switch (value?.toLowerCase()) { + case "none": { + return "none" + } + case "low": { + return "low" + } + case "medium": { + return "medium" + } + case "high": { + return "high" + } + case "xhigh": { + return "xhigh" + } + case "max": { + return "max" + } + default: { + return undefined + } + } +} + +const normalizeClaudeOpus47Effort = ( + value: string | undefined | null, +): ClaudeOpus47Effort | undefined => { + switch (value?.toLowerCase()) { + case "low": { + return "low" + } + case "medium": { + return "medium" + } + case "high": { + return "high" + } + case "xhigh": { + return "xhigh" + } + case "max": { + return "max" + } + default: { + return undefined + } + } +} + +const getRequestedClaudeOpus47Effort = ( + payload: ChatCompletionsPayload, + claudeSettingsEnv: Record, +): ClaudeOpus47Effort | undefined => { + if (!isClaudeOpus47Model(payload.model)) { + return undefined + } + + const raw = + payload.output_config?.effort + ?? normalizeClaudeOpus47Effort(payload.reasoning_effort) + ?? normalizeClaudeOpus47Effort(process.env.COPILOT_REASONING_EFFORT) + ?? normalizeClaudeOpus47Effort(claudeSettingsEnv.COPILOT_REASONING_EFFORT) + + return sanitizeClaudeEffortForModel(payload.model, raw) +} + +export const sanitizeUserIdentifier = ( + user: string | null | undefined, +): string | undefined => { + if (!user) { + return undefined + } + + return user.slice(0, MAX_USER_LENGTH) +} + +const buildRequestPayload = ( + payload: ChatCompletionsPayload, + claudeSettingsEnv: Record, +): ChatCompletionsRequestPayload => { + const requestedReasoningEffort = getRequestedReasoningEffort( + payload, + claudeSettingsEnv, + ) + const requestedClaudeOpus47Effort = getRequestedClaudeOpus47Effort( + payload, + claudeSettingsEnv, + ) + + const reasoningEffort = + ( + usesMaxCompletionTokens(payload.model) + && payload.tools !== null + && payload.tools !== undefined + && payload.tools.length > 0 + ) ? + undefined + : requestedReasoningEffort + + if ( + !usesMaxCompletionTokens(payload.model) + || payload.max_tokens === null + || payload.max_tokens === undefined + ) { + const sanitizedPayload = { + ...payload, + output_config: + requestedClaudeOpus47Effort ? + { + ...payload.output_config, + effort: requestedClaudeOpus47Effort, + } + : payload.output_config, + reasoning_effort: + isClaudeOpus47Model(payload.model) ? undefined : ( + payload.reasoning_effort + ), + user: sanitizeUserIdentifier(payload.user), + } + + return reasoningEffort === null || reasoningEffort === undefined ? + sanitizedPayload + : { ...sanitizedPayload, reasoning_effort: reasoningEffort } + } + + return { + ...payload, + max_tokens: undefined, + max_completion_tokens: payload.max_tokens, + reasoning_effort: reasoningEffort, + user: sanitizeUserIdentifier(payload.user), + } +} + export const createChatCompletions = async ( payload: ChatCompletionsPayload, ) => { @@ -28,13 +270,24 @@ export const createChatCompletions = async ( "X-Initiator": isAgentCall ? "agent" : "user", } + const claudeSettingsEnv = await getClaudeSettingsEnv() + const requestPayload = buildRequestPayload(payload, claudeSettingsEnv) + + if (shouldUseResponsesApiForModel(payload.model)) { + return createResponses(payload, headers, claudeSettingsEnv) + } + const response = await fetch(`${copilotBaseUrl(state)}/chat/completions`, { method: "POST", headers, - body: JSON.stringify(payload), + body: JSON.stringify(requestPayload), }) if (!response.ok) { + if (await shouldRetryWithResponses(response)) { + return createResponses(payload, headers, claudeSettingsEnv) + } + consola.error("Failed to create chat completions", response) throw new HTTPError("Failed to create chat completions", response) } @@ -46,6 +299,52 @@ export const createChatCompletions = async ( return (await response.json()) as ChatCompletionResponse } +async function createResponses( + payload: ChatCompletionsPayload, + headers: Record, + claudeSettingsEnv: Record, +) { + const reasoningEffort = getRequestedReasoningEffort( + payload, + claudeSettingsEnv, + ) as ResponsesReasoningEffort | undefined + + const response = await fetch(`${copilotBaseUrl(state)}/responses`, { + method: "POST", + headers, + body: JSON.stringify( + buildResponsesRequestPayload(payload, reasoningEffort), + ), + }) + + if (!response.ok) { + consola.error("Failed to create responses", response) + throw new HTTPError("Failed to create responses", response) + } + + if (payload.stream) { + return translateResponsesStreamToChatCompletionStream(events(response)) + } + + return translateResponsesToChatCompletion( + (await response.json()) as ResponsesApiResponse, + ) +} + +async function shouldRetryWithResponses(response: Response): Promise { + try { + const errorBody = (await response.clone().json()) as { + error?: { + code?: string + } + } + + return errorBody.error?.code === "unsupported_api_for_model" + } catch { + return false + } +} + // Streaming types export interface ChatCompletionChunk { @@ -130,6 +429,14 @@ export interface ChatCompletionsPayload { temperature?: number | null top_p?: number | null max_tokens?: number | null + thinking?: { + type: "enabled" | "adaptive" + budget_tokens?: number + } | null + output_config?: { + effort?: "low" | "medium" | "high" | "xhigh" | "max" + } | null + reasoning_effort?: "none" | "low" | "medium" | "high" | "max" | "xhigh" | null stop?: string | Array | null n?: number | null stream?: boolean | null @@ -150,6 +457,14 @@ export interface ChatCompletionsPayload { user?: string | null } +type ChatCompletionsRequestPayload = Omit< + ChatCompletionsPayload, + "max_tokens" +> & { + max_tokens?: number | null + max_completion_tokens?: number | null +} + export interface Tool { type: "function" function: { diff --git a/tests/anthropic-request.test.ts b/tests/anthropic-request.test.ts index 06c663778..b214b9e88 100644 --- a/tests/anthropic-request.test.ts +++ b/tests/anthropic-request.test.ts @@ -199,6 +199,124 @@ describe("Anthropic to OpenAI translation logic", () => { }) }) +describe("opus-4.7 thinking + effort translation", () => { + // Note: translateToOpenAI calls resolveModelId which falls back to + // stripSnapshotSuffix when state.models is empty. "claude-opus-4.7", + // "claude-sonnet-4", and "gpt-5" all survive that path unchanged. + + test("thinking.enabled passes through verbatim (NOT adaptive)", () => { + const payload: AnthropicMessagesPayload = { + model: "claude-opus-4.7", + max_tokens: 64, + messages: [{ role: "user", content: "hi" }], + thinking: { type: "enabled", budget_tokens: 4096 }, + } + const out = translateToOpenAI(payload) + expect(out.thinking).toEqual({ type: "enabled", budget_tokens: 4096 }) + }) + + test("thinking.enabled without budget_tokens", () => { + const payload: AnthropicMessagesPayload = { + model: "claude-opus-4.7", + max_tokens: 64, + messages: [{ role: "user", content: "hi" }], + thinking: { type: "enabled" }, + } + const out = translateToOpenAI(payload) + expect(out.thinking?.type).toBe("enabled") + }) + + test("thinking.adaptive is coerced to enabled (upstream rejects adaptive)", () => { + const payload: AnthropicMessagesPayload = { + model: "claude-opus-4.7", + max_tokens: 64, + messages: [{ role: "user", content: "hi" }], + thinking: { type: "adaptive" }, + } + const out = translateToOpenAI(payload) + expect(out.thinking?.type).toBe("enabled") + }) + + test("reasoning_effort=high downgraded to medium", () => { + const payload: AnthropicMessagesPayload = { + model: "claude-opus-4.7", + max_tokens: 64, + messages: [{ role: "user", content: "hi" }], + reasoning_effort: "high", + } + const out = translateToOpenAI(payload) + expect(out.output_config?.effort).toBe("medium") + }) + + test("reasoning_effort=max downgraded to medium", () => { + const payload: AnthropicMessagesPayload = { + model: "claude-opus-4.7", + max_tokens: 64, + messages: [{ role: "user", content: "hi" }], + reasoning_effort: "max", + } + const out = translateToOpenAI(payload) + expect(out.output_config?.effort).toBe("medium") + }) + + test("budget_tokens=24576 (would be high) capped to medium", () => { + const payload: AnthropicMessagesPayload = { + model: "claude-opus-4.7", + max_tokens: 64, + messages: [{ role: "user", content: "hi" }], + thinking: { type: "enabled", budget_tokens: 24_576 }, + } + const out = translateToOpenAI(payload) + expect(out.output_config?.effort).toBe("medium") + }) + + test("reasoning_effort=medium kept as medium", () => { + const payload: AnthropicMessagesPayload = { + model: "claude-opus-4.7", + max_tokens: 64, + messages: [{ role: "user", content: "hi" }], + reasoning_effort: "medium", + } + const out = translateToOpenAI(payload) + expect(out.output_config?.effort).toBe("medium") + }) + + test("reasoning_effort=low kept as low", () => { + const payload: AnthropicMessagesPayload = { + model: "claude-opus-4.7", + max_tokens: 64, + messages: [{ role: "user", content: "hi" }], + reasoning_effort: "low", + } + const out = translateToOpenAI(payload) + expect(out.output_config?.effort).toBe("low") + }) + + test("gpt-5 path unaffected: reasoning_effort goes through, no output_config", () => { + const payload: AnthropicMessagesPayload = { + model: "gpt-5", + max_tokens: 64, + messages: [{ role: "user", content: "hi" }], + reasoning_effort: "high", + } + const out = translateToOpenAI(payload) + expect(out.output_config).toBeUndefined() + expect(out.reasoning_effort).toBe("high") + }) + + test("non-opus claude: no thinking, no output_config injected", () => { + const payload: AnthropicMessagesPayload = { + model: "claude-sonnet-4", + max_tokens: 64, + messages: [{ role: "user", content: "hi" }], + thinking: { type: "enabled" }, + } + const out = translateToOpenAI(payload) + expect(out.thinking).toBeUndefined() + expect(out.output_config).toBeUndefined() + }) +}) + describe("OpenAI Chat Completion v1 Request Payload Validation with Zod", () => { test("should return true for a minimal valid request payload", () => { const validPayload = { From e6b8ed28f07f6bc049263e5266e1c773b9cc8597 Mon Sep 17 00:00:00 2001 From: godlockin Date: Sat, 25 Apr 2026 23:47:22 +0800 Subject: [PATCH 06/10] docs: add CLAUDE.md project guide and claude-copilot.sh launcher Co-Authored-By: Claude Opus 4 --- CLAUDE.md | 80 +++++++++++++++++++++++++++ claude-copilot.sh | 136 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 216 insertions(+) create mode 100644 CLAUDE.md create mode 100755 claude-copilot.sh diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..4006345bc --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,80 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Build & Development Commands + +```sh +bun install # Install dependencies +bun run dev # Run in dev mode with hot reload +bun run build # Build for production (tsdown) +bun run start # Run production build +bun run lint # Lint with eslint +bun run lint:all # Lint all files +bun run typecheck # Type check with tsc +bun test # Run all tests +bun test tests/foo.test.ts # Run single test file +``` + +Pre-commit hooks run `lint --fix` via lint-staged automatically. + +## Architecture Overview + +**Core Structure:** +- `src/main.ts` - CLI entry point using `citty`, defines subcommands (start, auth, check-usage, debug) +- `src/start.ts` - Main server startup logic, handles authentication flow +- `src/server.ts` - Hono server with routes for OpenAI & Anthropic compatible APIs +- `src/auth.ts` - GitHub OAuth device flow authentication + +**Request Flow:** +1. CLI command parsed by citty → `start.ts` handles auth/token refresh +2. Token stored in `~/.local/share/copilot-api/` via `src/lib/paths.ts` +3. Server starts on port 4141 (default) with Hono +4. Incoming requests translated from OpenAI/Anthropic format → GitHub Copilot API + +**Key Services:** +- `src/services/github/*` - GitHub API calls (device code, access token, user, copilot token) +- `src/services/copilot/*` - Copilot API calls (chat completions, embeddings, models) +- `src/routes/*` - API route handlers with format translation + +**Libraries:** +- `src/lib/*` - Rate limiting, manual approval, proxy, tokenizer, state management + +**Route Structure:** +- `/v1/chat/completions` - OpenAI compatible (translates to Copilot API) +- `/v1/messages` - Anthropic Messages API compatible +- `/v1/models` - Lists available models +- `/v1/embeddings` - OpenAI compatible embeddings +- `/usage` - Copilot usage/quota monitoring +- `/token` - Current Copilot token info + +## Code Style + +- **Imports:** ESNext modules, use `~/*` path alias for `src/*` +- **Types:** Strict mode, explicit types, no `any` +- **Naming:** `camelCase` variables/functions, `PascalCase` types/classes +- **Error Handling:** Use custom error classes from `src/lib/error.ts` +- **Testing:** Bun test runner, tests in `tests/*.test.ts` + +## Configuration Files + +- `tsconfig.json` - TypeScript config with path aliases (`~/*` → `src/*`) +- `tsdown.config.ts` - Build configuration (ESM, es2022 target, Node platform) +- `eslint.config.js` - Linting rules (@echristian/eslint-config) +- `package.json` - Scripts, dependencies, lint-staged config + +## State & Data Management + +**Runtime State:** `src/lib/state.ts` - In-memory state object storing: +- GitHub/Copilot tokens +- Account type, models, VSCode version cache +- Rate limiting config and request timestamps +- Manual approval flag + +**Persistent Data:** Tokens stored in `~/.local/share/copilot-api/` (see `src/lib/paths.ts`) + +## Testing Patterns + +- Test files in `tests/*.test.ts` +- Examples: `anthropic-request.test.ts`, `anthropic-response.test.ts`, `create-chat-completions.test.ts` +- Tests focus on request/response translation between OpenAI/Anthropic formats and Copilot API diff --git a/claude-copilot.sh b/claude-copilot.sh new file mode 100755 index 000000000..a6ad7f3fc --- /dev/null +++ b/claude-copilot.sh @@ -0,0 +1,136 @@ +#!/usr/bin/env bash +# ============================================================ +# claude-copilot - Start Copilot API and launch Claude Code +# ============================================================ + +set -e + +# Configuration +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PORT="${PORT:-1234}" +MODEL="${MODEL:-claude-sonnet-4.6}" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +log_info() { echo -e "${BLUE}▶${NC} $1"; } +log_success() { echo -e "${GREEN}✓${NC} $1"; } +log_warn() { echo -e "${YELLOW}!${NC} $1"; } +log_error() { echo -e "${RED}✗${NC} $1"; } + +# ============================================================ +# Step 1: Check dependencies +# ============================================================ +check_dependencies() { + log_info "Checking dependencies..." + + # Check bun + if command -v bun &> /dev/null; then + log_success "bun found: $(bun --version)" + else + log_error "bun not found. Installing..." + npm install -g bun + log_success "bun installed" + fi + + # Check node_modules + if [[ ! -d "$SCRIPT_DIR/node_modules" ]]; then + log_info "Installing dependencies..." + cd "$SCRIPT_DIR" && bun install + log_success "Dependencies installed" + else + log_success "Dependencies already installed" + fi +} + +# ============================================================ +# Step 2: Check authentication +# ============================================================ +check_auth() { + log_info "Checking GitHub authentication..." + + TOKEN_DIR="$HOME/.local/share/copilot-api" + STATE_FILE="$TOKEN_DIR/state.json" + + if [[ -f "$STATE_FILE" ]]; then + log_success "Already authenticated" + else + log_warn "Not authenticated, running GitHub auth flow..." + cd "$SCRIPT_DIR" && bun run ./src/main.ts auth + log_success "Authentication completed" + fi +} + +# ============================================================ +# Step 3: Start the server +# ============================================================ +start_server() { + log_info "Starting Copilot API server on port $PORT..." + + # Check if port is already in use + if lsof -i :$PORT &> /dev/null; then + log_warn "Port $PORT is already in use" + log_info "Using existing server at http://localhost:$PORT" + return 0 + fi + + # Start server in background + cd "$SCRIPT_DIR" + nohup bun run ./src/main.ts start --port "$PORT" > "$SCRIPT_DIR/server.log" 2>&1 & + SERVER_PID=$! + echo $SERVER_PID > "$SCRIPT_DIR/.server.pid" + + # Wait for server to start + log_info "Waiting for server to start..." + for i in {1..30}; do + if curl -s "http://localhost:$PORT/v1/models" &> /dev/null; then + log_success "Server started successfully at http://localhost:$PORT" + return 0 + fi + sleep 1 + done + + log_error "Server failed to start. Check logs: $SCRIPT_DIR/server.log" + return 1 +} + +# ============================================================ +# Step 4: Launch Claude with Copilot configuration +# ============================================================ +launch_claude() { + log_info "Launching Claude Code with Copilot API..." + + if command -v claude &> /dev/null; then + ANTHROPIC_AUTH_TOKEN="copilot-api" \ + ANTHROPIC_BASE_URL="http://localhost:$PORT/v1" \ + ANTHROPIC_MODEL="$MODEL" \ + API_TIMEOUT_MS="300000" \ + CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC="1" \ + claude "$@" + else + log_error "Claude Code not found. Please install with: npm install -g @anthropic-ai/claude-code" + exit 1 + fi +} + +# ============================================================ +# Main +# ============================================================ +main() { + echo "" + echo "╔════════════════════════════════════════════════════════╗" + echo "║ claude-copilot - Copilot API + Claude │" + echo "╚════════════════════════════════════════════════════════╝" + echo "" + + check_dependencies + check_auth + start_server + launch_claude "$@" +} + +main "$@" From 818ca37bee6b4ed7b39d2fb8f914dcdf2467433f Mon Sep 17 00:00:00 2001 From: godlockin Date: Sun, 26 Apr 2026 14:53:06 +0800 Subject: [PATCH 07/10] feat(opus-4.7): open effort cap to low/medium/high/xhigh/max MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Probed upstream Copilot directly: opus-4.7 now accepts all effort levels (low/medium/high/xhigh/max) without 400, and reasoning_effort shows real gradient in completion tokens (low~214 → max~374) on a fixed prompt with thinking enabled. Lift the medium cap so high-effort requests reach the model instead of being silently downgraded. Tests updated to reflect the new pass-through behavior. Co-Authored-By: Claude Opus 4 --- src/routes/messages/non-stream-translation.ts | 11 ++++++++++- src/services/copilot/create-chat-completions.ts | 9 ++++++++- tests/anthropic-request.test.ts | 12 ++++++------ 3 files changed, 24 insertions(+), 8 deletions(-) diff --git a/src/routes/messages/non-stream-translation.ts b/src/routes/messages/non-stream-translation.ts index 62dd79fd2..6301d7dbb 100644 --- a/src/routes/messages/non-stream-translation.ts +++ b/src/routes/messages/non-stream-translation.ts @@ -63,7 +63,16 @@ type ClaudeOpus47Effort = NonNullable< // Per-model effort caps. Upstream rejects efforts above the model's tier. // TODO: keep in sync with the mirror in services/copilot/create-chat-completions.ts. -const OPUS_47_ALLOWED_EFFORTS: Array = ["low", "medium"] +// 2026-04 probe: upstream now accepts low/medium/high/xhigh/max for opus-4.7 +// (and silently ignores unknown values), so the cap is opened up. Keep the +// helper in place so we can re-tighten without restructuring callers. +const OPUS_47_ALLOWED_EFFORTS: Array = [ + "low", + "medium", + "high", + "xhigh", + "max", +] function getAllowedClaudeEfforts(modelId: string): Array { if (isClaudeOpus47Model(modelId)) { diff --git a/src/services/copilot/create-chat-completions.ts b/src/services/copilot/create-chat-completions.ts index 9840d4e27..454430a3a 100644 --- a/src/services/copilot/create-chat-completions.ts +++ b/src/services/copilot/create-chat-completions.ts @@ -25,7 +25,14 @@ type ClaudeOpus47Effort = NonNullable< // Per-model effort caps for Claude. Mirror of the helper in // routes/messages/non-stream-translation.ts — keep in sync. -const OPUS_47_ALLOWED_EFFORTS: Array = ["low", "medium"] +// 2026-04 probe: upstream now accepts low/medium/high/xhigh/max for opus-4.7. +const OPUS_47_ALLOWED_EFFORTS: Array = [ + "low", + "medium", + "high", + "xhigh", + "max", +] export const getAllowedClaudeEfforts = ( modelId: string, diff --git a/tests/anthropic-request.test.ts b/tests/anthropic-request.test.ts index b214b9e88..0baaf2dbb 100644 --- a/tests/anthropic-request.test.ts +++ b/tests/anthropic-request.test.ts @@ -237,7 +237,7 @@ describe("opus-4.7 thinking + effort translation", () => { expect(out.thinking?.type).toBe("enabled") }) - test("reasoning_effort=high downgraded to medium", () => { + test("reasoning_effort=high passes through (cap opened up)", () => { const payload: AnthropicMessagesPayload = { model: "claude-opus-4.7", max_tokens: 64, @@ -245,10 +245,10 @@ describe("opus-4.7 thinking + effort translation", () => { reasoning_effort: "high", } const out = translateToOpenAI(payload) - expect(out.output_config?.effort).toBe("medium") + expect(out.output_config?.effort).toBe("high") }) - test("reasoning_effort=max downgraded to medium", () => { + test("reasoning_effort=max passes through (cap opened up)", () => { const payload: AnthropicMessagesPayload = { model: "claude-opus-4.7", max_tokens: 64, @@ -256,10 +256,10 @@ describe("opus-4.7 thinking + effort translation", () => { reasoning_effort: "max", } const out = translateToOpenAI(payload) - expect(out.output_config?.effort).toBe("medium") + expect(out.output_config?.effort).toBe("max") }) - test("budget_tokens=24576 (would be high) capped to medium", () => { + test("budget_tokens=24576 maps to high (cap opened up)", () => { const payload: AnthropicMessagesPayload = { model: "claude-opus-4.7", max_tokens: 64, @@ -267,7 +267,7 @@ describe("opus-4.7 thinking + effort translation", () => { thinking: { type: "enabled", budget_tokens: 24_576 }, } const out = translateToOpenAI(payload) - expect(out.output_config?.effort).toBe("medium") + expect(out.output_config?.effort).toBe("high") }) test("reasoning_effort=medium kept as medium", () => { From d543f8686e1c73fda83b5398fea5e001b46da389 Mon Sep 17 00:00:00 2001 From: godlockin Date: Sun, 26 Apr 2026 14:54:27 +0800 Subject: [PATCH 08/10] feat(usage): add local HTML viewer with auto-refresh MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - New /usage/view route serving a self-contained dark-mode dashboard rendering quota_snapshots (chat/completions/premium_interactions) with progress bars, plan/SKU/reset metadata, and refresh controls. - /usage now content-negotiates: text/html → viewer, JSON otherwise, preserving API compatibility. - Auto-refresh defaults to 30s, with on/off toggle and 10s/30s/60s/5m selector; pauses on tab hidden, persists prefs to localStorage, honors ?endpoint, ?auto, ?refresh URL params. - claude-copilot.sh: add --daemon-start/--daemon-stop subcommands and print log path + usage URL after server is ready. Co-Authored-By: Claude Opus 4 --- claude-copilot.sh | 47 +++++++- src/routes/usage/route.ts | 14 +++ src/routes/usage/viewer.ts | 239 +++++++++++++++++++++++++++++++++++++ 3 files changed, 296 insertions(+), 4 deletions(-) create mode 100644 src/routes/usage/viewer.ts diff --git a/claude-copilot.sh b/claude-copilot.sh index a6ad7f3fc..8e8cd33b9 100755 --- a/claude-copilot.sh +++ b/claude-copilot.sh @@ -89,6 +89,7 @@ start_server() { for i in {1..30}; do if curl -s "http://localhost:$PORT/v1/models" &> /dev/null; then log_success "Server started successfully at http://localhost:$PORT" + print_daemon_info return 0 fi sleep 1 @@ -98,6 +99,18 @@ start_server() { return 1 } +# ============================================================ +# Print daemon info (log dir + usage URL) +# ============================================================ +print_daemon_info() { + echo "" + log_info "Log file: $SCRIPT_DIR/server.log" + log_info "Log dir: $SCRIPT_DIR" + log_info "Usage page: http://localhost:$PORT/usage" + log_info "Token info: http://localhost:$PORT/token" + echo "" +} + # ============================================================ # Step 4: Launch Claude with Copilot configuration # ============================================================ @@ -127,10 +140,36 @@ main() { echo "╚════════════════════════════════════════════════════════╝" echo "" - check_dependencies - check_auth - start_server - launch_claude "$@" + case "${1:-}" in + --daemon-start) + shift + check_dependencies + check_auth + start_server + log_success "Daemon started. Use 'claude-copilot --daemon-stop' to stop." + exit 0 + ;; + --daemon-stop) + if [[ -f "$SCRIPT_DIR/.server.pid" ]]; then + PID=$(cat "$SCRIPT_DIR/.server.pid") + if kill "$PID" 2>/dev/null; then + log_success "Daemon stopped (PID $PID)" + else + log_warn "PID $PID not running" + fi + rm -f "$SCRIPT_DIR/.server.pid" + else + log_warn "No PID file found at $SCRIPT_DIR/.server.pid" + fi + exit 0 + ;; + *) + check_dependencies + check_auth + start_server + launch_claude "$@" + ;; + esac } main "$@" diff --git a/src/routes/usage/route.ts b/src/routes/usage/route.ts index 3e9473236..6ba6b6eff 100644 --- a/src/routes/usage/route.ts +++ b/src/routes/usage/route.ts @@ -2,9 +2,21 @@ import { Hono } from "hono" import { getCopilotUsage } from "~/services/github/get-copilot-usage" +import { usageViewerHtml } from "./viewer" + export const usageRoute = new Hono() usageRoute.get("/", async (c) => { + // Content negotiation: serve HTML viewer to browsers, JSON to API clients. + const accept = c.req.header("accept") ?? "" + const wantsHtml = + c.req.query("view") === "html" + || (accept.includes("text/html") && !accept.includes("application/json")) + + if (wantsHtml) { + return c.html(usageViewerHtml) + } + try { const usage = await getCopilotUsage() return c.json(usage) @@ -13,3 +25,5 @@ usageRoute.get("/", async (c) => { return c.json({ error: "Failed to fetch Copilot usage" }, 500) } }) + +usageRoute.get("/view", (c) => c.html(usageViewerHtml)) diff --git a/src/routes/usage/viewer.ts b/src/routes/usage/viewer.ts new file mode 100644 index 000000000..6084b38ab --- /dev/null +++ b/src/routes/usage/viewer.ts @@ -0,0 +1,239 @@ +// Self-contained HTML viewer for /usage. Renders Copilot quota snapshots +// (chat / completions / premium_interactions) plus plan + reset metadata. +// Reads from same-origin /usage by default; ?endpoint=... overrides. +export const usageViewerHtml = /* html */ ` + + + + +Copilot Usage + + + +
+

Copilot Usage

+ + + + +
+
+
+ + + +` From 8a6b55177e50878d687e099ecd47baf362440840 Mon Sep 17 00:00:00 2001 From: godlockin Date: Sun, 26 Apr 2026 23:07:19 +0800 Subject: [PATCH 09/10] feat: cherry-pick setup.sh installer + claude-copilot.sh daemon improvements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Brings in the installer/docs/launcher work from godlockin/master (aa4bdaf) while keeping main's adaptive→enabled thinking translation and opened-up opus-4.7 effort cap (low/medium/high/xhigh/max) — master had reverted non-stream-translation.ts which silently drops the thinking field and caused upstream 400s for clients sending thinking.adaptive. - setup.sh: 5-step installer (deps, build, auth, shell config, launchd plist) - claude-copilot.sh: --daemon shows local dashboard URL (usage/view) - README: quick start, model tier table, effort support docs - .gitignore: add copilot-data/ docker volume exclusion - bun.lock: dependency updates from master Co-Authored-By: Claude Opus 4 --- .gitignore | 5 +- README.md | 77 ++++++ bun.lock | 153 +++++++++++- claude-copilot.sh | 616 ++++++++++++++++++++++++++++++++++------------ setup.sh | 317 ++++++++++++++++++++++++ 5 files changed, 1001 insertions(+), 167 deletions(-) create mode 100755 setup.sh diff --git a/.gitignore b/.gitignore index 605d69266..00fb5f946 100644 --- a/.gitignore +++ b/.gitignore @@ -17,4 +17,7 @@ dist/ server.log server.pid .eket/ -.eket-*.log \ No newline at end of file +.eket-*.log + +# docker data +copilot-data/ diff --git a/README.md b/README.md index 0d36c13c9..650ff47b5 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,83 @@ A reverse-engineered proxy for the GitHub Copilot API that exposes it as an Open https://github.com/user-attachments/assets/7654b383-669d-4eb9-b23c-06d7aefee8c5 +## Quick Start: Claude Code with Copilot + +最快上手方式——一键安装并启动 Claude Code: + +```bash +# 1. 安装引导(检查依赖、认证、配置 shell、生成 launchd plist) +./setup.sh + +# 2. 重载 shell 配置 +source ~/.zshrc # 或 source ~/.bashrc + +# 3. 启动 +claude-copilot # 默认 sonnet 模型 +claude-copilot -m opus # opus-4.7,高推理 effort +claude-copilot -m opus-med # opus-4.7,中推理 effort(推荐日常使用) +claude-copilot -m haiku # haiku,省配额 +``` + +### 模型档位说明 + +`claude-copilot` 支持三层模型分工,Claude Code 会根据任务复杂度自动选择: + +| 环境变量 | 用途 | 默认配置 | +|---------|------|---------| +| `ANTHROPIC_DEFAULT_OPUS_MODEL` | 复杂任务:规划、架构决策、多步推理 | `claude-opus-4.7` | +| `ANTHROPIC_MODEL` / `ANTHROPIC_DEFAULT_SONNET_MODEL` | 常规任务:写代码、单测、调试 | `claude-sonnet-4.6` | +| `ANTHROPIC_SMALL_FAST_MODEL` / `ANTHROPIC_DEFAULT_HAIKU_MODEL` | 轻量任务:工具调用、文件读写、背景任务 | `claude-haiku-4.5` | + +**`-m` 参数对应关系:** + +| 参数 | 主模型 | Effort | 适用场景 | +|-----|-------|--------|---------| +| `sonnet`(默认)| sonnet-4.6 | 无 | 日常编码,subagent 用 opus-4.7 | +| `opus` / `opus47` / `opus-high` | opus-4.7 | `high` | 深度推理、复杂架构 | +| `opus-med` / `opus47-med` | opus-4.7 | `medium` | 均衡,推荐大多数重任务 | +| `opus46` | opus-4.6 | 无 | opus-4.6(无 effort 支持) | +| `haiku` | haiku-4.5 | 无 | 省配额、快速验证 | + +> **Reasoning Effort 说明**:仅 `claude-opus-4.7` 支持 effort(`low/medium/high/xhigh/max`)。 +> `opus-4.6`、`sonnet`、`haiku` 设置 effort 无效,proxy 会自动丢弃。 + +### 常用命令 + +```bash +claude-copilot --status # 检查服务、认证、可用模型 +claude-copilot --usage # 查看配额用量(premium interactions 等) +claude-copilot --stop # 停止服务 +claude-copilot --daemon # 守护模式(自动重启,Ctrl+C 退出) +claude-copilot --setup # 详细环境检查向导 +claude-copilot --port 4141 # 使用自定义端口 +``` + +### 环境变量配置 + +可通过以下环境变量覆盖默认行为(在 shell 配置文件中设置): + +```bash +export COPILOT_PORT=1234 # 服务端口 +export COPILOT_SCRIPT_DIR=/path/to/repo # copilot-api 源码目录 +export COPILOT_TIMEOUT_MS=3000000 # API 超时(毫秒) +export COPILOT_AUTH_FILE=~/.local/share/copilot-api/github_token # token 路径 +``` + +### Shell 集成 + +`claude-copilot.sh` 是一个可 source 的 shell 函数文件,支持 zsh / bash: + +```bash +# 手动添加到 ~/.zshrc 或 ~/.bashrc +export COPILOT_SCRIPT_DIR="/path/to/copilot-api" +source "$COPILOT_SCRIPT_DIR/claude-copilot.sh" +``` + +或直接运行 `./setup.sh` 自动完成配置。 + +--- + ## Prerequisites - Bun (>= 1.2.x) diff --git a/bun.lock b/bun.lock index 64ce2bfd4..8ed1cc38e 100644 --- a/bun.lock +++ b/bun.lock @@ -19,6 +19,7 @@ }, "devDependencies": { "@echristian/eslint-config": "^0.0.54", + "@eslint/markdown": "^8.0.1", "@types/bun": "^1.2.23", "@types/proxy-from-env": "^1.0.4", "bumpp": "^10.2.3", @@ -77,7 +78,7 @@ "@eslint/config-helpers": ["@eslint/config-helpers@0.4.0", "", { "dependencies": { "@eslint/core": "^0.16.0" } }, "sha512-WUFvV4WoIwW8Bv0KeKCIIEgdSiFOsulyN0xrMu+7z43q/hkOLXjvb5u7UC9jDxvRzcrbEmuZBX5yJZz1741jog=="], - "@eslint/core": ["@eslint/core@0.16.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q=="], + "@eslint/core": ["@eslint/core@1.2.1", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ=="], "@eslint/eslintrc": ["@eslint/eslintrc@3.3.1", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ=="], @@ -85,9 +86,11 @@ "@eslint/json": ["@eslint/json@0.13.2", "", { "dependencies": { "@eslint/core": "^0.15.2", "@eslint/plugin-kit": "^0.3.5", "@humanwhocodes/momoa": "^3.3.9", "natural-compare": "^1.4.0" } }, "sha512-yWLyRE18rHgHXhWigRpiyv1LDPkvWtC6oa7QHXW7YdP6gosJoq7BiLZW2yCs9U7zN7X4U3ZeOJjepA10XAOIMw=="], + "@eslint/markdown": ["@eslint/markdown@8.0.1", "", { "dependencies": { "@eslint/core": "^1.1.1", "@eslint/plugin-kit": "^0.6.1", "github-slugger": "^2.0.0", "mdast-util-from-markdown": "^2.0.2", "mdast-util-frontmatter": "^2.0.1", "mdast-util-gfm": "^3.1.0", "mdast-util-math": "^3.0.0", "micromark-extension-frontmatter": "^2.0.0", "micromark-extension-gfm": "^3.0.0", "micromark-extension-math": "^3.1.0", "micromark-util-normalize-identifier": "^2.0.1" } }, "sha512-WWKmld/EyNdEB8GMq7JMPX1SDWgyJAM1uhtCi5ySrqYQM4HQjmg11EX/q3ZpnpRXHfdccFtli3NBvvGaYjWyQw=="], + "@eslint/object-schema": ["@eslint/object-schema@2.1.6", "", {}, "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA=="], - "@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.0", "", { "dependencies": { "@eslint/core": "^0.16.0", "levn": "^0.4.1" } }, "sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A=="], + "@eslint/plugin-kit": ["@eslint/plugin-kit@0.6.1", "", { "dependencies": { "@eslint/core": "^1.1.1", "levn": "^0.4.1" } }, "sha512-iH1B076HoAshH1mLpHMgwdGeTs0CYwL0SPMkGuSebZrwBp16v415e9NZXg2jtrqPVQjf6IANe2Vtlr5KswtcZQ=="], "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], @@ -199,16 +202,28 @@ "@types/bun": ["@types/bun@1.2.23", "", { "dependencies": { "bun-types": "1.2.23" } }, "sha512-le8ueOY5b6VKYf19xT3McVbXqLqmxzPXHsQT/q9JHgikJ2X22wyTW3g3ohz2ZMnp7dod6aduIiq8A14Xyimm0A=="], + "@types/debug": ["@types/debug@4.1.13", "", { "dependencies": { "@types/ms": "*" } }, "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw=="], + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + "@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="], + "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], + "@types/katex": ["@types/katex@0.16.8", "", {}, "sha512-trgaNyfU+Xh2Tc+ABIb44a5AYUpicB3uwirOioeOkNPPbmgRNtcWyDeeFRzjPZENO9Vq8gvVqfhaaXWLlevVwg=="], + + "@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="], + + "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], + "@types/node": ["@types/node@24.6.2", "", { "dependencies": { "undici-types": "~7.13.0" } }, "sha512-d2L25Y4j+W3ZlNAeMKcy7yDsK425ibcAOO2t7aPTz6gNMH0z2GThtwENCDc0d/Pw9wgyRqE5Px1wkV7naz8ang=="], "@types/proxy-from-env": ["@types/proxy-from-env@1.0.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-TPR9/bCZAr3V1eHN4G3LD3OLicdJjqX1QRXWuNcCYgE66f/K8jO2ZRtHxI2D9MbnuUP6+qiKSS8eUHp6TFHGCw=="], "@types/react": ["@types/react@19.2.0", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-1LOH8xovvsKsCBq1wnT4ntDUdCJKmnEakhsuoUSy6ExlHCkGP2hqnatagYTgFk6oeL0VU31u7SNjunPN+GchtA=="], + "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.45.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.45.0", "@typescript-eslint/type-utils": "8.45.0", "@typescript-eslint/utils": "8.45.0", "@typescript-eslint/visitor-keys": "8.45.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.45.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-HC3y9CVuevvWCl/oyZuI47dOeDF9ztdMEfMH8/DW/Mhwa9cCLnK1oD7JoTVGW/u7kFzNZUKUoyJEqkaJh5y3Wg=="], "@typescript-eslint/parser": ["@typescript-eslint/parser@8.45.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.45.0", "@typescript-eslint/types": "8.45.0", "@typescript-eslint/typescript-estree": "8.45.0", "@typescript-eslint/visitor-keys": "8.45.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-TGf22kon8KW+DeKaUmOibKWktRY8b2NSAZNdtWh798COm1NWx8+xJ6iFBtk3IvLdv6+LGLJLRlyhrhEDZWargQ=="], @@ -305,10 +320,14 @@ "caniuse-lite": ["caniuse-lite@1.0.30001747", "", {}, "sha512-mzFa2DGIhuc5490Nd/G31xN1pnBnYMadtkyTjefPI7wzypqgCEpeWu9bJr0OnDsyKrW75zA9ZAt7pbQFmwLsQg=="], + "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], + "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], "change-case": ["change-case@5.4.4", "", {}, "sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w=="], + "character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="], + "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], "ci-info": ["ci-info@4.3.0", "", {}, "sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ=="], @@ -361,6 +380,8 @@ "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + "decode-named-character-reference": ["decode-named-character-reference@1.3.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q=="], + "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], "define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="], @@ -369,12 +390,16 @@ "defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="], + "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], + "destr": ["destr@2.0.5", "", {}, "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA=="], "detect-indent": ["detect-indent@7.0.2", "", {}, "sha512-y+8xyqdGLL+6sh0tVeHcfP/QDd8gUgbasolJJpY7NgeQGSZ739bDtSiaiDgtoicy+mtYB81dKLxO9xRhCyIB3A=="], "detect-newline": ["detect-newline@4.0.1", "", {}, "sha512-qE3Veg1YXzGHQhlA6jzebZN2qVf6NX+A7m7qlhCGG30dJixrAQhYOsJjsnBjJkCSmuOPpCk30145fr8FV0bzog=="], + "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="], + "diff": ["diff@8.0.2", "", {}, "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg=="], "dotenv": ["dotenv@17.2.3", "", {}, "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w=="], @@ -479,6 +504,8 @@ "fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="], + "fault": ["fault@2.0.1", "", { "dependencies": { "format": "^0.2.0" } }, "sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ=="], + "fd-package-json": ["fd-package-json@2.0.0", "", { "dependencies": { "walk-up-path": "^4.0.0" } }, "sha512-jKmm9YtsNXN789RS/0mSzOC1NUq9mkVd65vbSSVsKdjGvYXBuE4oWe2QOEoFeRmJg+lPuZxpmrfFclNhoRMneQ=="], "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], @@ -501,6 +528,8 @@ "for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="], + "format": ["format@0.2.2", "", {}, "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww=="], + "formatly": ["formatly@0.3.0", "", { "dependencies": { "fd-package-json": "^2.0.0" }, "bin": { "formatly": "bin/index.mjs" } }, "sha512-9XNj/o4wrRFyhSMJOvsuyMwy8aUfBaZ1VrqHVfohyXf0Sw0e+yfKG+xZaY3arGCOMdwFsqObtzVOc1gU9KiT9w=="], "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], @@ -529,6 +558,8 @@ "git-hooks-list": ["git-hooks-list@4.1.1", "", {}, "sha512-cmP497iLq54AZnv4YRAEMnEyQ1eIn4tGKbmswqwmFV4GBnAqE8NLtWxxdXa++AalfgL5EBH4IxTPyquEuGY/jA=="], + "github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="], + "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], "globals": ["globals@16.4.0", "", {}, "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw=="], @@ -665,6 +696,8 @@ "jsx-ast-utils": ["jsx-ast-utils@3.3.5", "", { "dependencies": { "array-includes": "^3.1.6", "array.prototype.flat": "^1.3.1", "object.assign": "^4.1.4", "object.values": "^1.1.6" } }, "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ=="], + "katex": ["katex@0.16.45", "", { "dependencies": { "commander": "^8.3.0" }, "bin": { "katex": "cli.js" } }, "sha512-pQpZbdBu7wCTmQUh7ufPmLr0pFoObnGUoL/yhtwJDgmmQpbkg/0HSVti25Fu4rmd1oCR6NGWe9vqTWuWv3GcNA=="], + "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], "knip": ["knip@5.64.1", "", { "dependencies": { "@nodelib/fs.walk": "^1.2.3", "fast-glob": "^3.3.3", "formatly": "^0.3.0", "jiti": "^2.6.0", "js-yaml": "^4.1.0", "minimist": "^1.2.8", "oxc-resolver": "^11.8.3", "picocolors": "^1.1.1", "picomatch": "^4.0.1", "smol-toml": "^1.4.1", "strip-json-comments": "5.0.2", "zod": "^4.1.11" }, "peerDependencies": { "@types/node": ">=18", "typescript": ">=5.0.4 <7" }, "bin": { "knip": "bin/knip.js", "knip-bun": "bin/knip-bun.js" } }, "sha512-80XnLsyeXuyxj1F4+NBtQFHxaRH0xWRw8EKwfQ6EkVZZ0bSz/kqqan08k/Qg8ajWsFPhFq+0S2RbLCBGIQtuOg=="], @@ -685,12 +718,102 @@ "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=="], + "longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="], + "magic-string": ["magic-string@0.30.19", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw=="], + "markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="], + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + "mdast-util-find-and-replace": ["mdast-util-find-and-replace@3.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "escape-string-regexp": "^5.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg=="], + + "mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.3", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q=="], + + "mdast-util-frontmatter": ["mdast-util-frontmatter@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "escape-string-regexp": "^5.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "micromark-extension-frontmatter": "^2.0.0" } }, "sha512-LRqI9+wdgC25P0URIJY9vwocIzCcksduHQ9OF2joxQoyTNVduwLAFUzjoopuRJbJAReaKrNQKAZKL3uCMugWJA=="], + + "mdast-util-gfm": ["mdast-util-gfm@3.1.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-gfm-autolink-literal": "^2.0.0", "mdast-util-gfm-footnote": "^2.0.0", "mdast-util-gfm-strikethrough": "^2.0.0", "mdast-util-gfm-table": "^2.0.0", "mdast-util-gfm-task-list-item": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ=="], + + "mdast-util-gfm-autolink-literal": ["mdast-util-gfm-autolink-literal@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "ccount": "^2.0.0", "devlop": "^1.0.0", "mdast-util-find-and-replace": "^3.0.0", "micromark-util-character": "^2.0.0" } }, "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ=="], + + "mdast-util-gfm-footnote": ["mdast-util-gfm-footnote@2.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0" } }, "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ=="], + + "mdast-util-gfm-strikethrough": ["mdast-util-gfm-strikethrough@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg=="], + + "mdast-util-gfm-table": ["mdast-util-gfm-table@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "markdown-table": "^3.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg=="], + + "mdast-util-gfm-task-list-item": ["mdast-util-gfm-task-list-item@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ=="], + + "mdast-util-math": ["mdast-util-math@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "longest-streak": "^3.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.1.0", "unist-util-remove-position": "^5.0.0" } }, "sha512-Tl9GBNeG/AhJnQM221bJR2HPvLOSnLE/T9cJI9tlc6zwQk2nPk/4f0cHkOdEixQPC/j8UtKDdITswvLAy1OZ1w=="], + + "mdast-util-phrasing": ["mdast-util-phrasing@4.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "unist-util-is": "^6.0.0" } }, "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w=="], + + "mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA=="], + + "mdast-util-to-string": ["mdast-util-to-string@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0" } }, "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg=="], + "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], + "micromark": ["micromark@4.0.2", "", { "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA=="], + + "micromark-core-commonmark": ["micromark-core-commonmark@2.0.3", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg=="], + + "micromark-extension-frontmatter": ["micromark-extension-frontmatter@2.0.0", "", { "dependencies": { "fault": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-C4AkuM3dA58cgZha7zVnuVxBhDsbttIMiytjgsM2XbHAB2faRVaHRle40558FBN+DJcrLNCoqG5mlrpdU4cRtg=="], + + "micromark-extension-gfm": ["micromark-extension-gfm@3.0.0", "", { "dependencies": { "micromark-extension-gfm-autolink-literal": "^2.0.0", "micromark-extension-gfm-footnote": "^2.0.0", "micromark-extension-gfm-strikethrough": "^2.0.0", "micromark-extension-gfm-table": "^2.0.0", "micromark-extension-gfm-tagfilter": "^2.0.0", "micromark-extension-gfm-task-list-item": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w=="], + + "micromark-extension-gfm-autolink-literal": ["micromark-extension-gfm-autolink-literal@2.1.0", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw=="], + + "micromark-extension-gfm-footnote": ["micromark-extension-gfm-footnote@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw=="], + + "micromark-extension-gfm-strikethrough": ["micromark-extension-gfm-strikethrough@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw=="], + + "micromark-extension-gfm-table": ["micromark-extension-gfm-table@2.1.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg=="], + + "micromark-extension-gfm-tagfilter": ["micromark-extension-gfm-tagfilter@2.0.0", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg=="], + + "micromark-extension-gfm-task-list-item": ["micromark-extension-gfm-task-list-item@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw=="], + + "micromark-extension-math": ["micromark-extension-math@3.1.0", "", { "dependencies": { "@types/katex": "^0.16.0", "devlop": "^1.0.0", "katex": "^0.16.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-lvEqd+fHjATVs+2v/8kg9i5Q0AP2k85H0WUOwpIVvUML8BapsMvh1XAogmQjOCsLpoKRCVQqEkQBB3NhVBcsOg=="], + + "micromark-factory-destination": ["micromark-factory-destination@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA=="], + + "micromark-factory-label": ["micromark-factory-label@2.0.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg=="], + + "micromark-factory-space": ["micromark-factory-space@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg=="], + + "micromark-factory-title": ["micromark-factory-title@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw=="], + + "micromark-factory-whitespace": ["micromark-factory-whitespace@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ=="], + + "micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="], + + "micromark-util-chunked": ["micromark-util-chunked@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA=="], + + "micromark-util-classify-character": ["micromark-util-classify-character@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q=="], + + "micromark-util-combine-extensions": ["micromark-util-combine-extensions@2.0.1", "", { "dependencies": { "micromark-util-chunked": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg=="], + + "micromark-util-decode-numeric-character-reference": ["micromark-util-decode-numeric-character-reference@2.0.2", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw=="], + + "micromark-util-decode-string": ["micromark-util-decode-string@2.0.1", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ=="], + + "micromark-util-encode": ["micromark-util-encode@2.0.1", "", {}, "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw=="], + + "micromark-util-html-tag-name": ["micromark-util-html-tag-name@2.0.1", "", {}, "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA=="], + + "micromark-util-normalize-identifier": ["micromark-util-normalize-identifier@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q=="], + + "micromark-util-resolve-all": ["micromark-util-resolve-all@2.0.1", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg=="], + + "micromark-util-sanitize-uri": ["micromark-util-sanitize-uri@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ=="], + + "micromark-util-subtokenize": ["micromark-util-subtokenize@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA=="], + + "micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="], + + "micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="], + "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], "mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="], @@ -941,6 +1064,16 @@ "unicorn-magic": ["unicorn-magic@0.3.0", "", {}, "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA=="], + "unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="], + + "unist-util-remove-position": ["unist-util-remove-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q=="], + + "unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="], + + "unist-util-visit": ["unist-util-visit@5.1.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg=="], + + "unist-util-visit-parents": ["unist-util-visit-parents@6.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="], + "update-browserslist-db": ["update-browserslist-db@1.1.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw=="], "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], @@ -979,8 +1112,14 @@ "zod": ["zod@4.1.11", "", {}, "sha512-WPsqwxITS2tzx1bzhIKsEs19ABD5vmCVa4xBo2tq/SrV4RNZtfws1EnCWQXM6yh8bD08a1idvkB5MZSBiZsjwg=="], + "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], + "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], + "@eslint/compat/@eslint/core": ["@eslint/core@0.16.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q=="], + + "@eslint/config-helpers/@eslint/core": ["@eslint/core@0.16.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q=="], + "@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], "@eslint/eslintrc/strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], @@ -997,6 +1136,10 @@ "cliui/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], + "eslint/@eslint/core": ["@eslint/core@0.16.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q=="], + + "eslint/@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.0", "", { "dependencies": { "@eslint/core": "^0.16.0", "levn": "^0.4.1" } }, "sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A=="], + "eslint-plugin-unicorn/@eslint/plugin-kit": ["@eslint/plugin-kit@0.3.5", "", { "dependencies": { "@eslint/core": "^0.15.2", "levn": "^0.4.1" } }, "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w=="], "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], @@ -1005,6 +1148,12 @@ "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=="], + "katex/commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="], + + "mdast-util-find-and-replace/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], + + "mdast-util-frontmatter/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], + "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], "npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="], diff --git a/claude-copilot.sh b/claude-copilot.sh index 8e8cd33b9..f0e93a71f 100755 --- a/claude-copilot.sh +++ b/claude-copilot.sh @@ -1,175 +1,463 @@ #!/usr/bin/env bash -# ============================================================ -# claude-copilot - Start Copilot API and launch Claude Code -# ============================================================ - -set -e - -# Configuration -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PORT="${PORT:-1234}" -MODEL="${MODEL:-claude-sonnet-4.6}" - -# Colors for output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -NC='\033[0m' # No Color - -log_info() { echo -e "${BLUE}▶${NC} $1"; } -log_success() { echo -e "${GREEN}✓${NC} $1"; } -log_warn() { echo -e "${YELLOW}!${NC} $1"; } -log_error() { echo -e "${RED}✗${NC} $1"; } - -# ============================================================ -# Step 1: Check dependencies -# ============================================================ -check_dependencies() { - log_info "Checking dependencies..." - - # Check bun - if command -v bun &> /dev/null; then - log_success "bun found: $(bun --version)" - else - log_error "bun not found. Installing..." - npm install -g bun - log_success "bun installed" - fi - - # Check node_modules - if [[ ! -d "$SCRIPT_DIR/node_modules" ]]; then - log_info "Installing dependencies..." - cd "$SCRIPT_DIR" && bun install - log_success "Dependencies installed" - else - log_success "Dependencies already installed" - fi -} +# ============================================================================== +# claude-copilot — 用 GitHub Copilot 运行 Claude Code 的一键封装 +# +# 用法:source claude-copilot.sh 后执行 claude-copilot [options] [claude-args...] +# 或直接 source 到 ~/.zshrc / ~/.bashrc +# +# Options: +# --stop 停止本地 copilot-api 服务 +# --status 检查服务 / 认证 / 模型状态 +# --usage 查看 Copilot 用量配额 +# --daemon 守护模式(自动重启,Ctrl+C 退出) +# --setup 完整环境检查和设置向导 +# --port 自定义端口(默认 1234) +# -m|--model 选择模型档位(见下方模型说明) +# +# 模型档位(-m 参数): +# sonnet 默认。主: sonnet-4.6, 复杂subagent: opus-4.7, 工具: haiku-4.5 +# opus 主: opus-4.7 (effort=high), 编码: sonnet-4.6, 工具: haiku-4.5 +# opus47 同 opus +# opus-high 同 opus +# opus-med 主: opus-4.7 (effort=medium),均衡版 +# opus47-med 同 opus-med +# opus46 主: opus-4.6(无 effort 支持) +# haiku 全部使用 haiku-4.5,省配额 +# +# Reasoning Effort 说明: +# 仅 claude-opus-4.7 支持 effort (low/medium/high/xhigh/max) +# 其他模型(opus-4.6, sonnet, haiku)设置无效,proxy 会丢弃 +# +# 环境变量覆盖(可在调用前 export): +# COPILOT_PORT 服务端口(默认 1234) +# COPILOT_AUTH_FILE token 文件路径 +# COPILOT_TIMEOUT_MS API 超时毫秒数(默认 3000000) +# COPILOT_SCRIPT_DIR copilot-api 源码目录 +# ============================================================================== -# ============================================================ -# Step 2: Check authentication -# ============================================================ -check_auth() { - log_info "Checking GitHub authentication..." - - TOKEN_DIR="$HOME/.local/share/copilot-api" - STATE_FILE="$TOKEN_DIR/state.json" - - if [[ -f "$STATE_FILE" ]]; then - log_success "Already authenticated" - else - log_warn "Not authenticated, running GitHub auth flow..." - cd "$SCRIPT_DIR" && bun run ./src/main.ts auth - log_success "Authentication completed" - fi -} +claude-copilot() { + # -------------------------------------------------------------------------- + # Configuration defaults (overridable via env vars) + # -------------------------------------------------------------------------- + local COPILOT_PORT="${COPILOT_PORT:-1234}" + local COPILOT_HOST="http://localhost" + local COPILOT_BASE_URL="${COPILOT_HOST}:${COPILOT_PORT}" + local COPILOT_API_TIMEOUT="${COPILOT_TIMEOUT_MS:-3000000}" + local COPILOT_ACCOUNT_TYPE="business" + local SELECTED_MODEL="sonnet" + local REASONING_EFFORT="" + local ACTION="start" + local COPILOT_DIR="${COPILOT_SCRIPT_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}" + + # -------------------------------------------------------------------------- + # Logging helpers + # -------------------------------------------------------------------------- + local _R='\033[0;31m' _G='\033[0;32m' _Y='\033[1;33m' _B='\033[0;34m' _N='\033[0m' + log_info() { echo -e "${_B}▶${_N} $*"; } + log_success() { echo -e "${_G}✓${_N} $*"; } + log_warn() { echo -e "${_Y}!${_N} $*"; } + log_error() { echo -e "${_R}✗${_N} $*"; } + + # -------------------------------------------------------------------------- + # Argument parsing + # -------------------------------------------------------------------------- + while [[ $# -gt 0 ]]; do + case "$1" in + --stop) ACTION="stop"; shift ;; + --status) ACTION="status"; shift ;; + --usage) ACTION="usage"; shift ;; + --daemon) ACTION="daemon"; shift ;; + --setup) ACTION="setup"; shift ;; + --port) COPILOT_PORT="$2"; COPILOT_BASE_URL="${COPILOT_HOST}:${COPILOT_PORT}"; shift 2 ;; + -m|--model) SELECTED_MODEL="$2"; shift 2 ;; + --) shift; break ;; + -*) break ;; + *) break ;; + esac + done + + # -------------------------------------------------------------------------- + # Model tier resolution + # -------------------------------------------------------------------------- + # Tier design: + # ANTHROPIC_DEFAULT_OPUS_MODEL → complex tasks (planning, orchestration) + # ANTHROPIC_MODEL / SONNET → normal tasks (coding, testing) + # ANTHROPIC_SMALL_FAST_MODEL → simple tasks (tool calls, background) + # + # Note: COPILOT_REASONING_EFFORT only takes effect on claude-opus-4.7. + # All other models silently discard it. + local COPILOT_MODEL COPILOT_SONNET_MODEL COPILOT_OPUS_MODEL + local COPILOT_HAIKU_MODEL COPILOT_SMALL_MODEL + + case "$SELECTED_MODEL" in + opus|opus47|opus-high) + # Full power: opus-4.7 as main with high effort + COPILOT_MODEL="claude-opus-4.7" + COPILOT_SONNET_MODEL="claude-sonnet-4.6" + COPILOT_OPUS_MODEL="claude-opus-4.7" + COPILOT_HAIKU_MODEL="claude-haiku-4.5" + COPILOT_SMALL_MODEL="claude-haiku-4.5" + REASONING_EFFORT="high" + ;; + opus-med|opus47-med) + # Balanced: opus-4.7 with medium effort + COPILOT_MODEL="claude-opus-4.7" + COPILOT_SONNET_MODEL="claude-sonnet-4.6" + COPILOT_OPUS_MODEL="claude-opus-4.7" + COPILOT_HAIKU_MODEL="claude-haiku-4.5" + COPILOT_SMALL_MODEL="claude-haiku-4.5" + REASONING_EFFORT="medium" + ;; + opus46) + # opus-4.6 — no effort support + COPILOT_MODEL="claude-opus-4.6" + COPILOT_SONNET_MODEL="claude-sonnet-4.6" + COPILOT_OPUS_MODEL="claude-opus-4.6" + COPILOT_HAIKU_MODEL="claude-haiku-4.5" + COPILOT_SMALL_MODEL="claude-haiku-4.5" + REASONING_EFFORT="" + ;; + haiku) + # Fast/quota-efficient + COPILOT_MODEL="claude-haiku-4.5" + COPILOT_SONNET_MODEL="claude-sonnet-4.6" + COPILOT_OPUS_MODEL="claude-opus-4.7" + COPILOT_HAIKU_MODEL="claude-haiku-4.5" + COPILOT_SMALL_MODEL="claude-haiku-4.5" + REASONING_EFFORT="" + ;; + sonnet|*) + # Default: sonnet main, opus for heavy subagent tasks, haiku for tools + COPILOT_MODEL="claude-sonnet-4.6" + COPILOT_SONNET_MODEL="claude-sonnet-4.6" + COPILOT_OPUS_MODEL="claude-opus-4.7" + COPILOT_HAIKU_MODEL="claude-haiku-4.5" + COPILOT_SMALL_MODEL="claude-haiku-4.5" + REASONING_EFFORT="" + ;; + esac + + # -------------------------------------------------------------------------- + # Helper: check jq installed + # -------------------------------------------------------------------------- + _check_jq() { + if ! command -v jq &>/dev/null; then + log_warn "jq 未安装,正在通过 brew 安装..." + brew install jq || { log_error "jq 安装失败,请手动运行:brew install jq"; return 1; } + log_success "jq 安装完成" + fi + } + + # -------------------------------------------------------------------------- + # Helper: check server responding + # -------------------------------------------------------------------------- + _server_running() { + curl -sf "${COPILOT_BASE_URL}/v1/models" &>/dev/null + } + + # -------------------------------------------------------------------------- + # Helper: ensure service is running (start if not) + # -------------------------------------------------------------------------- + _ensure_service_running() { + if curl -s "${COPILOT_BASE_URL}/usage" &>/dev/null; then + log_success "copilot-api 服务已在端口 ${COPILOT_PORT} 运行" + return 0 + fi + + log_info "正在启动 copilot-api 服务..." + local bin="${COPILOT_DIR}/dist/main.js" + if [[ -f "$bin" ]]; then + node "$bin" start -p "${COPILOT_PORT}" -a "${COPILOT_ACCOUNT_TYPE}" & + else + npx copilot-api@latest start -p "${COPILOT_PORT}" -a "${COPILOT_ACCOUNT_TYPE}" & + fi + local pid=$! + + log_info "等待服务启动..." + for i in {1..30}; do + if curl -s "${COPILOT_BASE_URL}/usage" &>/dev/null; then + log_success "服务已启动 (PID: $pid)" + return 0 + fi + sleep 1 + done + log_error "服务启动超时,请检查日志:${COPILOT_DIR}/server.log" + return 1 + } -# ============================================================ -# Step 3: Start the server -# ============================================================ -start_server() { - log_info "Starting Copilot API server on port $PORT..." - - # Check if port is already in use - if lsof -i :$PORT &> /dev/null; then - log_warn "Port $PORT is already in use" - log_info "Using existing server at http://localhost:$PORT" - return 0 - fi - - # Start server in background - cd "$SCRIPT_DIR" - nohup bun run ./src/main.ts start --port "$PORT" > "$SCRIPT_DIR/server.log" 2>&1 & - SERVER_PID=$! - echo $SERVER_PID > "$SCRIPT_DIR/.server.pid" - - # Wait for server to start - log_info "Waiting for server to start..." - for i in {1..30}; do - if curl -s "http://localhost:$PORT/v1/models" &> /dev/null; then - log_success "Server started successfully at http://localhost:$PORT" - print_daemon_info - return 0 + # -------------------------------------------------------------------------- + # Helper: start service foreground (for daemon loop) + # -------------------------------------------------------------------------- + _start_service_fg() { + local bin="${COPILOT_DIR}/dist/main.js" + if [[ -f "$bin" ]]; then + node "$bin" start -p "${COPILOT_PORT}" -a "${COPILOT_ACCOUNT_TYPE}" + else + npx copilot-api@latest start -p "${COPILOT_PORT}" -a "${COPILOT_ACCOUNT_TYPE}" + fi + } + + # -------------------------------------------------------------------------- + # Action: stop + # -------------------------------------------------------------------------- + if [[ "$ACTION" == "stop" ]]; then + log_info "正在停止 copilot-api 服务 (port ${COPILOT_PORT})..." + local pid; pid=$(lsof -ti tcp:"${COPILOT_PORT}" 2>/dev/null | head -1) + if [[ -n "$pid" ]]; then + kill "$pid" 2>/dev/null + log_success "服务已停止 (PID: $pid)" + else + log_warn "未找到运行在端口 ${COPILOT_PORT} 的服务" + fi + return 0 fi - sleep 1 - done - log_error "Server failed to start. Check logs: $SCRIPT_DIR/server.log" - return 1 -} + # -------------------------------------------------------------------------- + # Action: daemon — watch loop with auto-restart + # -------------------------------------------------------------------------- + if [[ "$ACTION" == "daemon" ]]; then + local LOG_FILE="${COPILOT_DIR}/server.log" -# ============================================================ -# Print daemon info (log dir + usage URL) -# ============================================================ -print_daemon_info() { - echo "" - log_info "Log file: $SCRIPT_DIR/server.log" - log_info "Log dir: $SCRIPT_DIR" - log_info "Usage page: http://localhost:$PORT/usage" - log_info "Token info: http://localhost:$PORT/token" - echo "" -} + echo "╔══════════════════════════════════════════════════════════╗" + echo "║ copilot-api 守护模式 (Ctrl+C 停止) ║" + echo "╚══════════════════════════════════════════════════════════╝" + echo "端口: ${COPILOT_PORT}" + echo "日志: ${LOG_FILE}" + echo "用量: ${COPILOT_BASE_URL}/usage" + echo "本地面板: ${COPILOT_BASE_URL}/usage/view" + echo "Dashboard: https://ericc-ch.github.io/copilot-api?endpoint=${COPILOT_BASE_URL}/usage" + echo "" -# ============================================================ -# Step 4: Launch Claude with Copilot configuration -# ============================================================ -launch_claude() { - log_info "Launching Claude Code with Copilot API..." - - if command -v claude &> /dev/null; then - ANTHROPIC_AUTH_TOKEN="copilot-api" \ - ANTHROPIC_BASE_URL="http://localhost:$PORT/v1" \ - ANTHROPIC_MODEL="$MODEL" \ - API_TIMEOUT_MS="300000" \ - CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC="1" \ - claude "$@" - else - log_error "Claude Code not found. Please install with: npm install -g @anthropic-ai/claude-code" - exit 1 - fi -} + # Start service if not already running + if ! curl -s "${COPILOT_BASE_URL}/usage" &>/dev/null; then + log_info "正在启动服务..." + local bin="${COPILOT_DIR}/dist/main.js" + if [[ -f "$bin" ]]; then + nohup node "$bin" start -p "${COPILOT_PORT}" -a "${COPILOT_ACCOUNT_TYPE}" >> "$LOG_FILE" 2>&1 & + else + nohup npx copilot-api@latest start -p "${COPILOT_PORT}" -a "${COPILOT_ACCOUNT_TYPE}" >> "$LOG_FILE" 2>&1 & + fi + local svc_pid=$! + log_info "等待服务启动 (PID: $svc_pid)..." + for i in {1..30}; do + if curl -s "${COPILOT_BASE_URL}/usage" &>/dev/null; then + log_success "服务已启动 (PID: $svc_pid)" + break + fi + sleep 1 + done + if ! curl -s "${COPILOT_BASE_URL}/usage" &>/dev/null; then + log_error "服务启动失败,查看日志:$LOG_FILE" + return 1 + fi + else + log_success "服务已在运行 (PID: $(lsof -ti tcp:"${COPILOT_PORT}" 2>/dev/null | head -1))" + fi + + echo "" + log_info "开始输出日志 (Ctrl+C 退出 tail,服务继续运行)..." + echo "" + tail -f "$LOG_FILE" + return 0 + fi -# ============================================================ -# Main -# ============================================================ -main() { - echo "" - echo "╔════════════════════════════════════════════════════════╗" - echo "║ claude-copilot - Copilot API + Claude │" - echo "╚════════════════════════════════════════════════════════╝" - echo "" - - case "${1:-}" in - --daemon-start) - shift - check_dependencies - check_auth - start_server - log_success "Daemon started. Use 'claude-copilot --daemon-stop' to stop." - exit 0 - ;; - --daemon-stop) - if [[ -f "$SCRIPT_DIR/.server.pid" ]]; then - PID=$(cat "$SCRIPT_DIR/.server.pid") - if kill "$PID" 2>/dev/null; then - log_success "Daemon stopped (PID $PID)" + # -------------------------------------------------------------------------- + # Action: status + # -------------------------------------------------------------------------- + if [[ "$ACTION" == "status" ]]; then + echo "=== Copilot API Status (port ${COPILOT_PORT}) ===" + echo "" + echo "依赖:" + command -v jq &>/dev/null && echo " ✅ jq" || echo " ❌ jq (未安装)" + command -v curl &>/dev/null && echo " ✅ curl" || echo " ❌ curl (未安装)" + command -v node &>/dev/null && echo " ✅ node" || echo " ❌ node (未安装)" + command -v bun &>/dev/null && echo " ✅ bun" || echo " ⚠️ bun (可选)" + command -v docker &>/dev/null && echo " ✅ docker" || echo " ⚠️ docker (可选)" + + echo "" + echo "认证:" + local tf="$HOME/.local/share/copilot-api/github_token" + [[ -f "$tf" ]] && echo " ✅ Token 存在" || echo " ❌ Token 不存在 (运行 setup)" + + echo "" + echo "服务:" + if curl -s "${COPILOT_BASE_URL}/usage" &>/dev/null; then + echo " 🟢 运行中" + echo " Dashboard: https://ericc-ch.github.io/copilot-api?endpoint=${COPILOT_BASE_URL}/usage" + echo "" + echo " 可用 Claude 模型:" + curl -s "${COPILOT_BASE_URL}/v1/models" \ + | jq -r '.data[] | select(.id | contains("claude")) | " - \(.id)"' 2>/dev/null || true else - log_warn "PID $PID not running" + echo " 🔴 未运行" fi - rm -f "$SCRIPT_DIR/.server.pid" - else - log_warn "No PID file found at $SCRIPT_DIR/.server.pid" - fi - exit 0 - ;; - *) - check_dependencies - check_auth - start_server - launch_claude "$@" - ;; - esac -} + return 0 + fi + + # -------------------------------------------------------------------------- + # Action: usage + # -------------------------------------------------------------------------- + if [[ "$ACTION" == "usage" ]]; then + _check_jq || return 1 + echo "=== GitHub Copilot 用量 ===" + + if ! curl -s "${COPILOT_BASE_URL}/usage" &>/dev/null; then + log_error "服务未运行,请先启动:claude-copilot --status" + return 1 + fi + + local usage; usage=$(curl -s "${COPILOT_BASE_URL}/usage") + echo "" + echo "用户:$(echo "$usage" | jq -r '.login')" + echo "计划:$(echo "$usage" | jq -r '.copilot_plan')" + echo "组织:$(echo "$usage" | jq -r '.organization_list[0].name // "N/A"')" + echo "" + echo "配额:" + + local chat_unlimited; chat_unlimited=$(echo "$usage" | jq -r '.quota_snapshots.chat.unlimited') + if [[ "$chat_unlimited" == "true" ]]; then + echo " 💬 Chat: 无限" + else + echo " 💬 Chat: $(echo "$usage" | jq -r '.quota_snapshots.chat.remaining') 次剩余" + fi + + local comp_unlimited; comp_unlimited=$(echo "$usage" | jq -r '.quota_snapshots.completions.unlimited') + [[ "$comp_unlimited" == "true" ]] && echo " ⌨️ Completions: 无限" + + local pr pe pp + pr=$(echo "$usage" | jq -r '.quota_snapshots.premium_interactions.remaining') + pe=$(echo "$usage" | jq -r '.quota_snapshots.premium_interactions.entitlement') + pp=$(echo "$usage" | jq -r '.quota_snapshots.premium_interactions.percent_remaining') + echo " ⭐ Premium: ${pr}/${pe} (${pp}%)" + + echo "" + echo "重置时间:$(echo "$usage" | jq -r '.quota_reset_date' | cut -d'T' -f1)" + echo "Dashboard: https://ericc-ch.github.io/copilot-api?endpoint=${COPILOT_BASE_URL}/usage" + return 0 + fi + + # -------------------------------------------------------------------------- + # Action: setup — verbose environment check + first-run guide + # -------------------------------------------------------------------------- + if [[ "$ACTION" == "setup" ]]; then + echo "╔══════════════════════════════════════════════════════════╗" + echo "║ GitHub Copilot API 环境检查和设置 ║" + echo "╚══════════════════════════════════════════════════════════╝" + echo "" + + log_info "步骤 1/4: 检查依赖..." + _check_jq || return 1 + command -v curl &>/dev/null || { log_error "curl 未安装"; return 1; } + command -v node &>/dev/null || { log_error "node 未安装,请安装 Node.js"; return 1; } + log_success "依赖检查完成" + echo "" + + log_info "步骤 2/4: 检查认证..." + local tf="$HOME/.local/share/copilot-api/github_token" + if [[ -f "$tf" ]]; then + log_success "Token 已存在:$tf" + else + log_warn "未找到 token,请运行认证:" + echo " cd ${COPILOT_DIR} && node dist/main.js auth" + echo " 或:npx copilot-api@latest auth" + return 1 + fi + echo "" + + log_info "步骤 3/4: 确保服务运行..." + _ensure_service_running || return 1 + echo "" + + log_info "步骤 4/4: 验证模型..." + local cnt; cnt=$(curl -s "${COPILOT_BASE_URL}/v1/models" | jq -r '.data | length' 2>/dev/null || echo 0) + if [[ "$cnt" -gt 0 ]]; then + log_success "获取到 ${cnt} 个模型" + echo "" + echo "Claude 系列模型:" + curl -s "${COPILOT_BASE_URL}/v1/models" \ + | jq -r '.data[] | select(.id | contains("claude")) | " - \(.id)"' 2>/dev/null + else + log_warn "无法获取模型列表" + fi + echo "" + + echo "╔══════════════════════════════════════════════════════════╗" + echo "║ 设置完成! ║" + echo "╚══════════════════════════════════════════════════════════╝" + echo "" + echo "启动方式:" + echo " claude-copilot # sonnet (默认)" + echo " claude-copilot -m opus # opus-4.7 high effort" + echo " claude-copilot -m opus-med # opus-4.7 medium effort" + echo " claude-copilot -m haiku # 省配额" + return 0 + fi -main "$@" + # -------------------------------------------------------------------------- + # Action: start (default) — ensure service, launch claude + # -------------------------------------------------------------------------- + _ensure_service_running || return 1 + + log_success "已连接到 copilot-api (${COPILOT_BASE_URL})" + echo "" + echo "🤖 模型配置:" + local effort_display="${REASONING_EFFORT:+ (effort=$REASONING_EFFORT)}" + echo " 主模型: ${COPILOT_MODEL}${effort_display}" + echo " Sonnet: ${COPILOT_SONNET_MODEL} ← 编码/测试" + echo " Opus: ${COPILOT_OPUS_MODEL} ← 规划/复杂推理" + echo " Haiku: ${COPILOT_HAIKU_MODEL} ← 工具调用/背景任务" + echo "" + echo "📊 Dashboard: https://ericc-ch.github.io/copilot-api?endpoint=${COPILOT_BASE_URL}/usage" + echo "" + + # Inner launcher — subshell isolates env changes + _run_claude() { + ( + local AUTH_FILE="${COPILOT_AUTH_FILE:-$HOME/.local/share/copilot-api/github_token}" + export ANTHROPIC_BASE_URL="${COPILOT_BASE_URL}" + export ANTHROPIC_MODEL="${COPILOT_MODEL}" + export ANTHROPIC_DEFAULT_SONNET_MODEL="${COPILOT_SONNET_MODEL}" + export ANTHROPIC_SMALL_FAST_MODEL="${COPILOT_SMALL_MODEL}" + export ANTHROPIC_DEFAULT_HAIKU_MODEL="${COPILOT_HAIKU_MODEL}" + export ANTHROPIC_DEFAULT_OPUS_MODEL="${COPILOT_OPUS_MODEL}" + export DISABLE_NON_ESSENTIAL_MODEL_CALLS="1" + export CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC="1" + export API_TIMEOUT_MS="${COPILOT_API_TIMEOUT}" + export UV_THREADPOOL_SIZE="16" + export NODE_OPTIONS="${NODE_OPTIONS:---no-warnings}" + unset ANTHROPIC_AUTH_TOKEN ANTHROPIC_API_KEY + [[ -n "$REASONING_EFFORT" ]] && export COPILOT_REASONING_EFFORT="${REASONING_EFFORT}" + if [[ -f "$AUTH_FILE" ]]; then + export ANTHROPIC_API_KEY="$(cat "$AUTH_FILE")" + else + export ANTHROPIC_API_KEY="copilot" + fi + claude "$@" + ) + } + + # Launch with socket-error auto-retry + local _tmplog; _tmplog=$(mktemp -t claude-copilot.XXXXXX) + _run_claude "$@" 2> >(tee "$_tmplog" >&2) + local result=$? + + if [[ $result -ne 0 ]] && grep -qiE "socket.*closed|ECONNRESET|ETIMEDOUT|EPIPE|fetch failed" "$_tmplog" 2>/dev/null; then + echo "" + log_warn "Socket 错误,正在重启服务并重试..." + lsof -ti tcp:"${COPILOT_PORT}" | xargs kill -9 2>/dev/null + sleep 1 + _ensure_service_running && { + local waited=0 + while [[ $waited -lt 30 ]] && ! _server_running; do sleep 1; ((waited++)); done + _server_running && { + log_info "服务恢复,正在重试 claude session..." + _run_claude "$@" + result=$? + } + } + fi + + rm -f "$_tmplog" 2>/dev/null + unset -f _run_claude _check_jq _server_running _ensure_service_running _start_service_fg 2>/dev/null + return $result +} diff --git a/setup.sh b/setup.sh new file mode 100755 index 000000000..1d22ba92a --- /dev/null +++ b/setup.sh @@ -0,0 +1,317 @@ +#!/usr/bin/env bash +# ============================================================================== +# setup.sh — copilot-api + claude-copilot 一键安装引导 +# +# 运行方式: +# ./setup.sh # 完整安装 +# ./setup.sh --check # 仅检查环境 +# ./setup.sh --auth # 仅重新认证 +# ============================================================================== + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Colors +_R='\033[0;31m' _G='\033[0;32m' _Y='\033[1;33m' _B='\033[0;34m' _C='\033[0;36m' _N='\033[0m' +log_info() { echo -e "${_B}▶${_N} $*"; } +log_success() { echo -e "${_G}✓${_N} $*"; } +log_warn() { echo -e "${_Y}!${_N} $*"; } +log_error() { echo -e "${_R}✗${_N} $*"; } +log_step() { echo -e "\n${_C}══ $* ${_N}"; } + +MODE="${1:-install}" + +echo "" +echo "╔══════════════════════════════════════════════════════════════╗" +echo "║ copilot-api + claude-copilot 安装向导 ║" +echo "╚══════════════════════════════════════════════════════════════╝" +echo "" + +# ============================================================================== +# Step 1: Check dependencies +# ============================================================================== +log_step "Step 1/5: 检查依赖" + +MISSING=() + +check_cmd() { + local cmd="$1" install_hint="$2" + if command -v "$cmd" &>/dev/null; then + log_success "$cmd: $(command -v "$cmd")" + else + log_warn "$cmd 未安装 → $install_hint" + MISSING+=("$cmd") + fi +} + +check_cmd "node" "https://nodejs.org 或 brew install node" +check_cmd "curl" "brew install curl" +check_cmd "jq" "brew install jq" +check_cmd "claude" "npm install -g @anthropic-ai/claude-code" + +# Optional +echo "" +echo "可选依赖:" +command -v bun &>/dev/null && log_success "bun (可选,用于本地构建)" || log_warn "bun 未安装 (可选) brew install bun" +command -v docker &>/dev/null && log_success "docker (可选,用于容器化运行)" || log_warn "docker 未安装 (可选)" + +if [[ ${#MISSING[@]} -gt 0 ]]; then + echo "" + log_error "缺少必要依赖: ${MISSING[*]}" + echo "" + echo "安装命令:" + for dep in "${MISSING[@]}"; do + case "$dep" in + node) echo " brew install node" ;; + curl) echo " brew install curl" ;; + jq) echo " brew install jq" ;; + claude) echo " npm install -g @anthropic-ai/claude-code" ;; + esac + done + echo "" + if [[ "$MODE" != "--check" ]]; then + read -r -p "是否尝试自动安装?[y/N] " ans + if [[ "$ans" =~ ^[Yy]$ ]]; then + for dep in "${MISSING[@]}"; do + case "$dep" in + node) brew install node ;; + curl) brew install curl ;; + jq) brew install jq ;; + claude) npm install -g @anthropic-ai/claude-code ;; + esac + done + else + log_error "请手动安装后重新运行 setup.sh" + exit 1 + fi + fi +fi + +[[ "$MODE" == "--check" ]] && { echo ""; log_success "环境检查完成"; exit 0; } + +# ============================================================================== +# Step 2: Build / install copilot-api +# ============================================================================== +log_step "Step 2/5: 安装 copilot-api" + +if [[ -f "${SCRIPT_DIR}/dist/main.js" ]]; then + log_success "已存在本地构建:${SCRIPT_DIR}/dist/main.js" + COPILOT_BIN="${SCRIPT_DIR}/dist/main.js" + COPILOT_RUN="node ${COPILOT_BIN}" +elif command -v bun &>/dev/null && [[ -f "${SCRIPT_DIR}/package.json" ]]; then + log_info "使用 bun 构建本地版本..." + cd "${SCRIPT_DIR}" + bun install + bun run build + log_success "构建完成:${SCRIPT_DIR}/dist/main.js" + COPILOT_BIN="${SCRIPT_DIR}/dist/main.js" + COPILOT_RUN="node ${COPILOT_BIN}" +else + log_warn "未找到本地构建,将使用 npx copilot-api@latest" + COPILOT_BIN="" + COPILOT_RUN="npx copilot-api@latest" +fi + +# ============================================================================== +# Step 3: GitHub Authentication +# ============================================================================== +log_step "Step 3/5: GitHub Copilot 认证" + +TOKEN_DIR="$HOME/.local/share/copilot-api" +TOKEN_FILE="${TOKEN_DIR}/github_token" +mkdir -p "$TOKEN_DIR" + +if [[ "$MODE" == "--auth" ]] || [[ ! -f "$TOKEN_FILE" ]]; then + if [[ -f "$TOKEN_FILE" ]]; then + BACKUP="${TOKEN_FILE}.backup.$(date +%Y%m%d%H%M%S)" + cp "$TOKEN_FILE" "$BACKUP" + log_info "已备份旧 token → $BACKUP" + fi + + log_info "启动 GitHub OAuth 设备流认证..." + echo "" + echo " 操作步骤:" + echo " 1. 复制下方显示的设备码" + echo " 2. 浏览器打开 https://github.com/login/device" + echo " 3. 粘贴设备码完成授权" + echo "" + + if [[ -n "$COPILOT_BIN" ]]; then + node "$COPILOT_BIN" auth + else + npx copilot-api@latest auth + fi + + if [[ -f "$TOKEN_FILE" ]]; then + log_success "认证成功,token 保存至:$TOKEN_FILE" + else + log_error "认证失败,未找到 token 文件" + exit 1 + fi +else + log_success "Token 已存在:$TOKEN_FILE" + echo " (如需重新认证,运行:./setup.sh --auth)" +fi + +[[ "$MODE" == "--auth" ]] && exit 0 + +# ============================================================================== +# Step 4: Configure shell (add source to zshrc/bashrc) +# ============================================================================== +log_step "Step 4/5: 配置 shell" + +SHELL_RC="" +if [[ -n "${ZSH_VERSION:-}" ]] || [[ "$SHELL" == */zsh ]]; then + SHELL_RC="$HOME/.zshrc" +elif [[ -n "${BASH_VERSION:-}" ]] || [[ "$SHELL" == */bash ]]; then + SHELL_RC="$HOME/.bashrc" +fi + +SOURCE_LINE="source \"${SCRIPT_DIR}/claude-copilot.sh\"" +EXPORT_LINE="export COPILOT_SCRIPT_DIR=\"${SCRIPT_DIR}\"" + +if [[ -n "$SHELL_RC" ]]; then + if grep -qF "claude-copilot.sh" "$SHELL_RC" 2>/dev/null; then + log_success "claude-copilot.sh 已在 $SHELL_RC 中配置" + else + echo "" >> "$SHELL_RC" + echo "# copilot-api + claude-copilot" >> "$SHELL_RC" + echo "$EXPORT_LINE" >> "$SHELL_RC" + echo "$SOURCE_LINE" >> "$SHELL_RC" + log_success "已添加到 $SHELL_RC" + echo "" + echo " 请运行以下命令使配置生效:" + echo " source $SHELL_RC" + fi +else + log_warn "无法检测 shell 类型,请手动添加到 shell 配置文件:" + echo "" + echo " $EXPORT_LINE" + echo " $SOURCE_LINE" +fi + +# ============================================================================== +# Step 5: Generate launchd plist (macOS only) +# ============================================================================== +log_step "Step 5/5: 配置 macOS 开机自启 (launchd)" + +PLIST_DIR="$HOME/Library/LaunchAgents" +PLIST_FILE="${PLIST_DIR}/dev.copilot-api.plist" +PORT="${COPILOT_PORT:-1234}" +LOG_FILE="${SCRIPT_DIR}/server.log" + +if [[ "$(uname)" != "Darwin" ]]; then + log_warn "非 macOS 系统,跳过 launchd 配置" +else + mkdir -p "$PLIST_DIR" + + if [[ -f "$PLIST_FILE" ]]; then + log_warn "launchd plist 已存在:$PLIST_FILE" + read -r -p "是否覆盖?[y/N] " ans + [[ "$ans" =~ ^[Yy]$ ]] || { log_info "跳过 launchd 配置"; echo ""; } + fi + + if [[ ! -f "$PLIST_FILE" ]] || [[ "${ans:-n}" =~ ^[Yy]$ ]]; then + # Determine node path + NODE_PATH="$(command -v node)" + + if [[ -n "$COPILOT_BIN" ]]; then + PROGRAM_ARG1="$NODE_PATH" + PROGRAM_ARG2="$COPILOT_BIN" + EXTRA_ARGS="start" + else + PROGRAM_ARG1="$(command -v npx)" + PROGRAM_ARG2="copilot-api@latest" + EXTRA_ARGS="start" + fi + + cat > "$PLIST_FILE" << PLIST + + + + + Label + dev.copilot-api + + ProgramArguments + + ${PROGRAM_ARG1} + ${PROGRAM_ARG2} + ${EXTRA_ARGS} + --port + ${PORT} + --account-type + business + + + EnvironmentVariables + + HOME + ${HOME} + PATH + /usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin + + + WorkingDirectory + ${SCRIPT_DIR} + + StandardOutPath + ${LOG_FILE} + + StandardErrorPath + ${LOG_FILE} + + RunAtLoad + + + KeepAlive + + + ThrottleInterval + 10 + + +PLIST + + log_success "launchd plist 已生成:$PLIST_FILE" + echo "" + echo " 加载服务(开机自启):" + echo " launchctl load $PLIST_FILE" + echo "" + echo " 立即启动:" + echo " launchctl start dev.copilot-api" + echo "" + echo " 查看状态:" + echo " launchctl list dev.copilot-api" + echo "" + + read -r -p "是否立即加载 launchd 服务?[y/N] " load_ans + if [[ "${load_ans:-n}" =~ ^[Yy]$ ]]; then + launchctl load "$PLIST_FILE" + log_success "launchd 服务已加载" + fi + fi +fi + +# ============================================================================== +# Done +# ============================================================================== +echo "" +echo "╔══════════════════════════════════════════════════════════════╗" +echo "║ 安装完成! ║" +echo "╚══════════════════════════════════════════════════════════════╝" +echo "" +echo "快速上手:" +echo " claude-copilot # 默认 sonnet,自动启动服务" +echo " claude-copilot -m opus # opus-4.7,高 effort" +echo " claude-copilot -m opus-med # opus-4.7,中 effort(均衡)" +echo " claude-copilot -m haiku # haiku,省配额" +echo " claude-copilot --status # 检查服务状态" +echo " claude-copilot --usage # 查看配额用量" +echo " claude-copilot --daemon # 守护模式(自动重启)" +echo "" +echo "如果 claude-copilot 命令不可用,请先运行:" +echo " source $SHELL_RC" +echo "" From 51d072c28aa8a341b2b7a713430c914c6b584153 Mon Sep 17 00:00:00 2001 From: godlockin Date: Mon, 27 Apr 2026 08:44:09 +0800 Subject: [PATCH 10/10] fix(opus-4.7): drop thinking field entirely; rely on output_config.effort MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Even after coercing thinking.adaptive→enabled, upstream Copilot still returned 400 "Input tag 'adaptive' found using 'type'" — likely because the validator scans the overall request body (including conversation content) for thinking-shaped strings, which is fragile when chat history mentions the word "adaptive". Effort already conveys reasoning intent (low→max); thinking is redundant for this provider. Stop forwarding thinking unconditionally. Tests: 3 thinking-translation cases updated to expect undefined. Probe: thinking.{adaptive,enabled+budget=31999} + reasoning_effort {low,medium,high,xhigh,max} all → 200. --- src/routes/messages/non-stream-translation.ts | 26 +++++++------------ tests/anthropic-request.test.ts | 12 ++++----- 2 files changed, 15 insertions(+), 23 deletions(-) diff --git a/src/routes/messages/non-stream-translation.ts b/src/routes/messages/non-stream-translation.ts index 6301d7dbb..01455f1fa 100644 --- a/src/routes/messages/non-stream-translation.ts +++ b/src/routes/messages/non-stream-translation.ts @@ -162,23 +162,15 @@ function getClaudeOpus47Effort( function translateThinking( payload: AnthropicMessagesPayload, ): ChatCompletionsPayload["thinking"] { - const modelId = translateModelName(payload.model) - - if (!isClaudeOpus47Model(modelId)) { - return undefined - } - - const t = payload.thinking - if (!t) { - return undefined - } - - // Upstream Copilot opus-4.7 only accepts {type: "enabled"} (no "adaptive", - // no "disabled"). The Anthropic schema admits "enabled" | "adaptive"; we - // coerce both to "enabled" so legacy clients sending "adaptive" keep working. - return t.budget_tokens === undefined ? - { type: "enabled" } - : { type: "enabled", budget_tokens: t.budget_tokens } + // Always drop the thinking field. Upstream Copilot opus-4.7 controls + // reasoning depth via output_config.effort instead. Forwarding the Anthropic + // thinking field has caused 400s ("Input tag 'adaptive'...") even after + // coercing type to "enabled" — the upstream validator appears to scan the + // overall request body, including conversation content, for thinking-shaped + // strings, which is fragile. Effort already conveys intent; thinking is + // redundant for this provider. + void payload + return undefined } function translateOutputConfig( diff --git a/tests/anthropic-request.test.ts b/tests/anthropic-request.test.ts index 0baaf2dbb..5c49fab04 100644 --- a/tests/anthropic-request.test.ts +++ b/tests/anthropic-request.test.ts @@ -204,7 +204,7 @@ describe("opus-4.7 thinking + effort translation", () => { // stripSnapshotSuffix when state.models is empty. "claude-opus-4.7", // "claude-sonnet-4", and "gpt-5" all survive that path unchanged. - test("thinking.enabled passes through verbatim (NOT adaptive)", () => { + test("thinking.enabled is dropped (upstream uses output_config.effort instead)", () => { const payload: AnthropicMessagesPayload = { model: "claude-opus-4.7", max_tokens: 64, @@ -212,10 +212,10 @@ describe("opus-4.7 thinking + effort translation", () => { thinking: { type: "enabled", budget_tokens: 4096 }, } const out = translateToOpenAI(payload) - expect(out.thinking).toEqual({ type: "enabled", budget_tokens: 4096 }) + expect(out.thinking).toBeUndefined() }) - test("thinking.enabled without budget_tokens", () => { + test("thinking.enabled without budget_tokens is also dropped", () => { const payload: AnthropicMessagesPayload = { model: "claude-opus-4.7", max_tokens: 64, @@ -223,10 +223,10 @@ describe("opus-4.7 thinking + effort translation", () => { thinking: { type: "enabled" }, } const out = translateToOpenAI(payload) - expect(out.thinking?.type).toBe("enabled") + expect(out.thinking).toBeUndefined() }) - test("thinking.adaptive is coerced to enabled (upstream rejects adaptive)", () => { + test("thinking.adaptive is dropped (no thinking field reaches upstream)", () => { const payload: AnthropicMessagesPayload = { model: "claude-opus-4.7", max_tokens: 64, @@ -234,7 +234,7 @@ describe("opus-4.7 thinking + effort translation", () => { thinking: { type: "adaptive" }, } const out = translateToOpenAI(payload) - expect(out.thinking?.type).toBe("enabled") + expect(out.thinking).toBeUndefined() }) test("reasoning_effort=high passes through (cap opened up)", () => {