@ashsec/copilot-api 0.11.2 → 0.11.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/main.js CHANGED
@@ -21,7 +21,7 @@ import util from "node:util";
21
21
 
22
22
  //#region package.json
23
23
  var name = "@ashsec/copilot-api";
24
- var version = "0.11.2";
24
+ var version = "0.11.5";
25
25
  var description = "Turn GitHub Copilot into OpenAI/Anthropic API compatible server. Usable with Claude Code!";
26
26
  var keywords = [
27
27
  "proxy",
@@ -100,7 +100,7 @@ var package_default = {
100
100
 
101
101
  //#endregion
102
102
  //#region src/lib/paths.ts
103
- const APP_DIR = path.join(os.homedir(), ".local", "share", "copilot-api");
103
+ const APP_DIR = process.env.DATA_DIR || path.join(os.homedir(), ".local", "share", "copilot-api");
104
104
  const GITHUB_TOKEN_PATH = path.join(APP_DIR, "github_token");
105
105
  const REPLACEMENTS_CONFIG_PATH = path.join(APP_DIR, "replacements.json");
106
106
  const PATHS = {
@@ -1538,6 +1538,59 @@ function generateEnvScript(envVars, commandToRun = "") {
1538
1538
  return commandBlock || commandToRun;
1539
1539
  }
1540
1540
 
1541
+ //#endregion
1542
+ //#region src/lib/ip-blocker.ts
1543
+ const ipTracker = /* @__PURE__ */ new Map();
1544
+ /**
1545
+ * Extracts the client IP from the x-forwarded-for header.
1546
+ * Returns the first IP in the comma-separated list, or null if header is absent.
1547
+ */
1548
+ function extractClientIp(c) {
1549
+ const xForwardedFor = c.req.header("x-forwarded-for");
1550
+ if (!xForwardedFor) return null;
1551
+ return xForwardedFor.split(",")[0]?.trim() || null;
1552
+ }
1553
+ /**
1554
+ * Gets the current UTC date as YYYY-MM-DD string.
1555
+ */
1556
+ function getUtcDateString() {
1557
+ return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
1558
+ }
1559
+ /**
1560
+ * Checks if an IP is blocked due to 3+ failed attempts today (UTC).
1561
+ * Cleans up stale entries (entries from previous days).
1562
+ */
1563
+ function isIpBlocked(ip) {
1564
+ const today = getUtcDateString();
1565
+ const entry = ipTracker.get(ip);
1566
+ if (!entry) return false;
1567
+ if (entry.date !== today) {
1568
+ ipTracker.delete(ip);
1569
+ return false;
1570
+ }
1571
+ return entry.count >= 3;
1572
+ }
1573
+ /**
1574
+ * Records a failed authentication attempt for an IP.
1575
+ * Increments count if entry exists for today, otherwise creates new entry.
1576
+ */
1577
+ function recordFailedAttempt(ip) {
1578
+ const today = getUtcDateString();
1579
+ const entry = ipTracker.get(ip);
1580
+ if (!entry) {
1581
+ ipTracker.set(ip, {
1582
+ count: 1,
1583
+ date: today
1584
+ });
1585
+ return;
1586
+ }
1587
+ if (entry.date === today) entry.count += 1;
1588
+ else ipTracker.set(ip, {
1589
+ count: 1,
1590
+ date: today
1591
+ });
1592
+ }
1593
+
1541
1594
  //#endregion
1542
1595
  //#region src/lib/request-auth.ts
1543
1596
  function normalizeApiKeys(apiKeys) {
@@ -1556,6 +1609,8 @@ function getConfiguredApiKeys() {
1556
1609
  function extractRequestApiKey(c) {
1557
1610
  const xApiKey = c.req.header("x-api-key")?.trim();
1558
1611
  if (xApiKey) return xApiKey;
1612
+ const googleApiKey = c.req.header("x-goog-api-key")?.trim();
1613
+ if (googleApiKey) return googleApiKey;
1559
1614
  const authorization = c.req.header("authorization");
1560
1615
  if (!authorization) return null;
1561
1616
  const [scheme, ...rest] = authorization.trim().split(/\s+/);
@@ -1597,10 +1652,16 @@ async function apiKeyGuard(c, next) {
1597
1652
  await next();
1598
1653
  return;
1599
1654
  }
1655
+ const clientIp = extractClientIp(c);
1656
+ if (clientIp !== null && isIpBlocked(clientIp)) {
1657
+ await new Promise(() => {});
1658
+ return;
1659
+ }
1600
1660
  if (extractRequestApiKey(c) === state.apiKeyAuth) {
1601
1661
  await next();
1602
1662
  return;
1603
1663
  }
1664
+ if (clientIp !== null) recordFailedAttempt(clientIp);
1604
1665
  await new Promise(() => {});
1605
1666
  }
1606
1667
 
@@ -1658,7 +1719,7 @@ async function logRawRequest(c) {
1658
1719
  const lines = [];
1659
1720
  lines.push(`${colors.magenta}${colors.bold}[DEBUG] Incoming Request${colors.reset}`, `${colors.cyan}${method}${colors.reset} ${url}`, `${colors.dim}Headers:${colors.reset}`);
1660
1721
  for (const [key, value] of Object.entries(headers)) {
1661
- const displayValue = key.toLowerCase().includes("authorization") ? `${value.slice(0, 20)}...` : value;
1722
+ const displayValue = key.toLowerCase().includes("authorization") || key.toLowerCase().includes("api-key") ? `${value.slice(0, 20)}...` : value;
1662
1723
  lines.push(` ${colors.gray}${key}:${colors.reset} ${displayValue}`);
1663
1724
  }
1664
1725
  if (method !== "GET" && method !== "HEAD") try {
@@ -1764,6 +1825,7 @@ const awaitApproval = async () => {
1764
1825
  */
1765
1826
  function normalizeModelName(model) {
1766
1827
  let normalized = model.replace("[1m]", "-1m");
1828
+ normalized = normalized.replace(/-\d{8}$/, "");
1767
1829
  normalized = normalized.replaceAll(/(\d)-(\d)/g, (_, p1, p2) => `${p1}.${p2}`);
1768
1830
  return normalized;
1769
1831
  }
@@ -2005,13 +2067,19 @@ const createChatCompletions = async (payload, options) => {
2005
2067
  ...copilotHeaders(state, enableVision),
2006
2068
  "X-Initiator": options?.initiator ?? (isAgentCall ? "agent" : "user")
2007
2069
  };
2070
+ if (payload.tools) for (const tool of payload.tools) {
2071
+ const params = tool.function.parameters;
2072
+ if (!params.type) params.type = "object";
2073
+ if (!params.properties) params.properties = {};
2074
+ }
2008
2075
  const response = await fetchWithRetry(`${copilotBaseUrl(state)}/chat/completions`, {
2009
2076
  method: "POST",
2010
2077
  headers,
2011
2078
  body: JSON.stringify(payload)
2012
2079
  });
2013
2080
  if (!response.ok) {
2014
- consola.error("Failed to create chat completions", response);
2081
+ const errorBody = await response.clone().text();
2082
+ consola.error("Failed to create chat completions", `Status: ${response.status}`, errorBody);
2015
2083
  throw new HTTPError("Failed to create chat completions", response);
2016
2084
  }
2017
2085
  if (payload.stream) return events(response);
@@ -2033,87 +2101,867 @@ async function handleCompletion$1(c) {
2033
2101
  };
2034
2102
  consola.debug("Request payload:", JSON.stringify(payload).slice(-400));
2035
2103
  setRequestContext(c, {
2036
- requestedModel,
2037
- provider: "ChatCompletions",
2038
- model: payload.model,
2039
- replacements: appliedRules,
2040
- reasoningEffort
2104
+ requestedModel,
2105
+ provider: "ChatCompletions",
2106
+ model: payload.model,
2107
+ replacements: appliedRules,
2108
+ reasoningEffort
2109
+ });
2110
+ const selectedModel = state.models?.data.find((model) => model.id === payload.model);
2111
+ try {
2112
+ if (selectedModel) {
2113
+ const tokenCount = await getTokenCount(payload, selectedModel);
2114
+ setRequestContext(c, { inputTokens: tokenCount.input });
2115
+ }
2116
+ } catch (error) {
2117
+ consola.warn("Failed to calculate token count:", error);
2118
+ }
2119
+ if (state.manualApprove) await awaitApproval();
2120
+ if (isNullish(payload.max_tokens)) {
2121
+ payload = {
2122
+ ...payload,
2123
+ max_tokens: selectedModel?.capabilities.limits.max_output_tokens
2124
+ };
2125
+ consola.debug("Set max_tokens to:", JSON.stringify(payload.max_tokens));
2126
+ }
2127
+ const response = await createChatCompletions(payload);
2128
+ if (isNonStreaming$1(response)) {
2129
+ consola.debug("Non-streaming response:", JSON.stringify(response));
2130
+ if (response.usage) setRequestContext(c, {
2131
+ inputTokens: response.usage.prompt_tokens,
2132
+ outputTokens: response.usage.completion_tokens
2133
+ });
2134
+ return c.json(response);
2135
+ }
2136
+ consola.debug("Streaming response");
2137
+ return streamSSE(c, async (stream) => {
2138
+ for await (const chunk of response) {
2139
+ consola.debug("Streaming chunk:", JSON.stringify(chunk));
2140
+ if (chunk.data && chunk.data !== "[DONE]") {
2141
+ const parsed = JSON.parse(chunk.data);
2142
+ if (parsed.usage) setRequestContext(c, {
2143
+ inputTokens: parsed.usage.prompt_tokens,
2144
+ outputTokens: parsed.usage.completion_tokens
2145
+ });
2146
+ }
2147
+ await stream.writeSSE(chunk);
2148
+ }
2149
+ });
2150
+ }
2151
+ const isNonStreaming$1 = (response) => Object.hasOwn(response, "choices");
2152
+
2153
+ //#endregion
2154
+ //#region src/routes/chat-completions/route.ts
2155
+ const completionRoutes = new Hono();
2156
+ completionRoutes.post("/", async (c) => {
2157
+ try {
2158
+ return await handleCompletion$1(c);
2159
+ } catch (error) {
2160
+ return await forwardError(c, error);
2161
+ }
2162
+ });
2163
+
2164
+ //#endregion
2165
+ //#region src/services/copilot/create-embeddings.ts
2166
+ const createEmbeddings = async (payload) => {
2167
+ if (!state.copilotToken) throw new Error("Copilot token not found");
2168
+ const response = await fetchWithRetry(`${copilotBaseUrl(state)}/embeddings`, {
2169
+ method: "POST",
2170
+ headers: copilotHeaders(state),
2171
+ body: JSON.stringify(payload)
2172
+ });
2173
+ if (!response.ok) throw new HTTPError("Failed to create embeddings", response);
2174
+ return await response.json();
2175
+ };
2176
+
2177
+ //#endregion
2178
+ //#region src/routes/embeddings/route.ts
2179
+ const embeddingRoutes = new Hono();
2180
+ embeddingRoutes.post("/", async (c) => {
2181
+ try {
2182
+ const paylod = await c.req.json();
2183
+ const response = await createEmbeddings(paylod);
2184
+ return c.json(response);
2185
+ } catch (error) {
2186
+ return await forwardError(c, error);
2187
+ }
2188
+ });
2189
+
2190
+ //#endregion
2191
+ //#region src/lib/logger.ts
2192
+ const LOG_RETENTION_MS = 10080 * 60 * 1e3;
2193
+ const CLEANUP_INTERVAL_MS = 1440 * 60 * 1e3;
2194
+ const LOG_DIR = path.join(PATHS.APP_DIR, "logs");
2195
+ const FLUSH_INTERVAL_MS = 1e3;
2196
+ const MAX_BUFFER_SIZE = 100;
2197
+ const logStreams = /* @__PURE__ */ new Map();
2198
+ const logBuffers = /* @__PURE__ */ new Map();
2199
+ const ensureLogDirectory = () => {
2200
+ if (!fs$1.existsSync(LOG_DIR)) fs$1.mkdirSync(LOG_DIR, { recursive: true });
2201
+ };
2202
+ const cleanupOldLogs = () => {
2203
+ if (!fs$1.existsSync(LOG_DIR)) return;
2204
+ const now = Date.now();
2205
+ for (const entry of fs$1.readdirSync(LOG_DIR)) {
2206
+ const filePath = path.join(LOG_DIR, entry);
2207
+ let stats;
2208
+ try {
2209
+ stats = fs$1.statSync(filePath);
2210
+ } catch {
2211
+ continue;
2212
+ }
2213
+ if (!stats.isFile()) continue;
2214
+ if (now - stats.mtimeMs > LOG_RETENTION_MS) try {
2215
+ fs$1.rmSync(filePath);
2216
+ } catch {
2217
+ continue;
2218
+ }
2219
+ }
2220
+ };
2221
+ const formatArgs = (args) => args.map((arg) => typeof arg === "string" ? arg : util.inspect(arg, {
2222
+ depth: null,
2223
+ colors: false
2224
+ })).join(" ");
2225
+ const sanitizeName = (name$1) => {
2226
+ const normalized = name$1.toLowerCase().replaceAll(/[^a-z0-9]+/g, "-").replaceAll(/^-+|-+$/g, "");
2227
+ return normalized === "" ? "handler" : normalized;
2228
+ };
2229
+ const getLogStream = (filePath) => {
2230
+ let stream = logStreams.get(filePath);
2231
+ if (!stream || stream.destroyed) {
2232
+ stream = fs$1.createWriteStream(filePath, { flags: "a" });
2233
+ logStreams.set(filePath, stream);
2234
+ stream.on("error", (error) => {
2235
+ console.warn("Log stream error", error);
2236
+ logStreams.delete(filePath);
2237
+ });
2238
+ }
2239
+ return stream;
2240
+ };
2241
+ const flushBuffer = (filePath) => {
2242
+ const buffer = logBuffers.get(filePath);
2243
+ if (!buffer || buffer.length === 0) return;
2244
+ const stream = getLogStream(filePath);
2245
+ const content = buffer.join("\n") + "\n";
2246
+ stream.write(content, (error) => {
2247
+ if (error) console.warn("Failed to write handler log", error);
2248
+ });
2249
+ logBuffers.set(filePath, []);
2250
+ };
2251
+ const flushAllBuffers = () => {
2252
+ for (const filePath of logBuffers.keys()) flushBuffer(filePath);
2253
+ };
2254
+ const appendLine = (filePath, line) => {
2255
+ let buffer = logBuffers.get(filePath);
2256
+ if (!buffer) {
2257
+ buffer = [];
2258
+ logBuffers.set(filePath, buffer);
2259
+ }
2260
+ buffer.push(line);
2261
+ if (buffer.length >= MAX_BUFFER_SIZE) flushBuffer(filePath);
2262
+ };
2263
+ setInterval(flushAllBuffers, FLUSH_INTERVAL_MS);
2264
+ const cleanup = () => {
2265
+ flushAllBuffers();
2266
+ for (const stream of logStreams.values()) stream.end();
2267
+ logStreams.clear();
2268
+ logBuffers.clear();
2269
+ };
2270
+ process.on("exit", cleanup);
2271
+ process.on("SIGINT", () => {
2272
+ cleanup();
2273
+ process.exit(0);
2274
+ });
2275
+ process.on("SIGTERM", () => {
2276
+ cleanup();
2277
+ process.exit(0);
2278
+ });
2279
+ let lastCleanup = 0;
2280
+ const createHandlerLogger = (name$1) => {
2281
+ ensureLogDirectory();
2282
+ const sanitizedName = sanitizeName(name$1);
2283
+ const instance = consola.withTag(name$1);
2284
+ if (state.verbose) instance.level = 5;
2285
+ instance.setReporters([]);
2286
+ instance.addReporter({ log(logObj) {
2287
+ ensureLogDirectory();
2288
+ if (Date.now() - lastCleanup > CLEANUP_INTERVAL_MS) {
2289
+ cleanupOldLogs();
2290
+ lastCleanup = Date.now();
2291
+ }
2292
+ const date = logObj.date;
2293
+ const dateKey = date.toLocaleDateString("sv-SE");
2294
+ const timestamp = date.toLocaleString("sv-SE", { hour12: false });
2295
+ const filePath = path.join(LOG_DIR, `${sanitizedName}-${dateKey}.log`);
2296
+ const message = formatArgs(logObj.args);
2297
+ const line = `[${timestamp}] [${logObj.type}] [${logObj.tag || name$1}]${message ? ` ${message}` : ""}`;
2298
+ appendLine(filePath, line);
2299
+ } });
2300
+ return instance;
2301
+ };
2302
+
2303
+ //#endregion
2304
+ //#region src/services/copilot/create-responses.ts
2305
+ const createResponses = async (payload, { vision, initiator }) => {
2306
+ if (!state.copilotToken) throw new Error("Copilot token not found");
2307
+ const headers = {
2308
+ ...copilotHeaders(state, vision),
2309
+ "X-Initiator": initiator
2310
+ };
2311
+ payload.service_tier = null;
2312
+ const response = await fetch(`${copilotBaseUrl(state)}/responses`, {
2313
+ method: "POST",
2314
+ headers,
2315
+ body: JSON.stringify(payload)
2316
+ });
2317
+ if (!response.ok) {
2318
+ consola.error("Failed to create responses", response);
2319
+ throw new HTTPError("Failed to create responses", response);
2320
+ }
2321
+ if (payload.stream) return events(response);
2322
+ return await response.json();
2323
+ };
2324
+
2325
+ //#endregion
2326
+ //#region src/routes/google-ai/request-translation.ts
2327
+ let toolCallCounter = 0;
2328
+ function nextToolCallId() {
2329
+ return `call_${Date.now()}_${toolCallCounter++}`;
2330
+ }
2331
+ function isTextPart(part) {
2332
+ return "text" in part;
2333
+ }
2334
+ function isFunctionCallPart(part) {
2335
+ return "functionCall" in part;
2336
+ }
2337
+ function isFunctionResponsePart(part) {
2338
+ return "functionResponse" in part;
2339
+ }
2340
+ /**
2341
+ * Convert Google contents array → OpenAI messages array.
2342
+ */
2343
+ function translateContents(contents) {
2344
+ const messages = [];
2345
+ for (const content of contents) if (content.role === "user") {
2346
+ const functionResponses = content.parts.filter((p) => isFunctionResponsePart(p));
2347
+ const otherParts = content.parts.filter((p) => !isFunctionResponsePart(p));
2348
+ for (const part of functionResponses) messages.push({
2349
+ role: "tool",
2350
+ tool_call_id: findToolCallId(messages, part.functionResponse.name),
2351
+ content: JSON.stringify(part.functionResponse.response)
2352
+ });
2353
+ if (otherParts.length > 0) {
2354
+ const textContent = otherParts.filter((p) => isTextPart(p)).map((p) => p.text).join("");
2355
+ if (textContent) messages.push({
2356
+ role: "user",
2357
+ content: textContent
2358
+ });
2359
+ }
2360
+ } else {
2361
+ const textParts = content.parts.filter((p) => isTextPart(p));
2362
+ const functionCalls = content.parts.filter((p) => isFunctionCallPart(p));
2363
+ const textContent = textParts.filter((p) => !p.thought).map((p) => p.text).join("");
2364
+ const toolCalls = functionCalls.length > 0 ? functionCalls.map((part) => ({
2365
+ id: nextToolCallId(),
2366
+ type: "function",
2367
+ function: {
2368
+ name: part.functionCall.name,
2369
+ arguments: JSON.stringify(part.functionCall.args)
2370
+ }
2371
+ })) : void 0;
2372
+ messages.push({
2373
+ role: "assistant",
2374
+ content: textContent || null,
2375
+ tool_calls: toolCalls
2376
+ });
2377
+ }
2378
+ return messages;
2379
+ }
2380
+ /**
2381
+ * Find the tool_call_id for a function response by walking back through messages
2382
+ * to find the matching assistant tool_call by function name.
2383
+ */
2384
+ function findToolCallId(messages, functionName) {
2385
+ for (let i = messages.length - 1; i >= 0; i--) {
2386
+ const msg = messages[i];
2387
+ if (msg.role === "assistant" && msg.tool_calls) {
2388
+ const match = msg.tool_calls.find((tc) => tc.function.name === functionName);
2389
+ if (match) return match.id;
2390
+ }
2391
+ }
2392
+ return nextToolCallId();
2393
+ }
2394
+ /**
2395
+ * Convert Google tools → OpenAI tools format.
2396
+ */
2397
+ function translateTools(tools) {
2398
+ if (!tools || tools.length === 0) return void 0;
2399
+ const openAITools = [];
2400
+ for (const tool of tools) if (tool.functionDeclarations) for (const decl of tool.functionDeclarations) openAITools.push({
2401
+ type: "function",
2402
+ function: {
2403
+ name: decl.name,
2404
+ description: decl.description,
2405
+ parameters: normalizeToolParameters(decl.parameters ?? {})
2406
+ }
2407
+ });
2408
+ return openAITools.length > 0 ? openAITools : void 0;
2409
+ }
2410
+ /**
2411
+ * Remove $schema from tool parameters and ensure valid structure.
2412
+ * Copilot requires parameters to have at least { type: "object", properties: {} }.
2413
+ */
2414
+ function normalizeToolParameters(params) {
2415
+ const cleaned = { ...params };
2416
+ delete cleaned.$schema;
2417
+ if (!cleaned.type) cleaned.type = "object";
2418
+ if (!cleaned.properties) cleaned.properties = {};
2419
+ return cleaned;
2420
+ }
2421
+ /**
2422
+ * Convert Google toolConfig → OpenAI tool_choice.
2423
+ */
2424
+ function translateToolChoice(toolConfig) {
2425
+ if (!toolConfig?.functionCallingConfig) return void 0;
2426
+ switch (toolConfig.functionCallingConfig.mode) {
2427
+ case "AUTO": return "auto";
2428
+ case "NONE": return "none";
2429
+ case "ANY": {
2430
+ const allowed = toolConfig.functionCallingConfig.allowedFunctionNames;
2431
+ if (allowed && allowed.length === 1) return {
2432
+ type: "function",
2433
+ function: { name: allowed[0] }
2434
+ };
2435
+ return "required";
2436
+ }
2437
+ default: return;
2438
+ }
2439
+ }
2440
+ /**
2441
+ * Extract system instruction from Google payload as an OpenAI system message.
2442
+ */
2443
+ function translateSystemInstruction(systemInstruction) {
2444
+ if (!systemInstruction?.parts) return void 0;
2445
+ const systemText = systemInstruction.parts.map((p) => p.text).join("\n");
2446
+ if (!systemText) return void 0;
2447
+ return {
2448
+ role: "system",
2449
+ content: systemText
2450
+ };
2451
+ }
2452
+ /**
2453
+ * Map Google generationConfig fields to OpenAI-compatible fields.
2454
+ */
2455
+ function mapGenerationConfigFields(config$1) {
2456
+ if (!config$1) return {};
2457
+ return {
2458
+ max_tokens: config$1.maxOutputTokens,
2459
+ temperature: config$1.temperature,
2460
+ top_p: config$1.topP,
2461
+ stop: config$1.stopSequences,
2462
+ seed: config$1.seed,
2463
+ frequency_penalty: config$1.frequencyPenalty,
2464
+ presence_penalty: config$1.presencePenalty,
2465
+ response_format: config$1.responseMimeType === "application/json" ? { type: "json_object" } : void 0
2466
+ };
2467
+ }
2468
+ /**
2469
+ * Convert Google generationConfig → OpenAI-compatible config fields.
2470
+ */
2471
+ function translateGenerationConfig(config$1, stream) {
2472
+ return {
2473
+ stream,
2474
+ stream_options: stream ? { include_usage: true } : void 0,
2475
+ ...mapGenerationConfigFields(config$1)
2476
+ };
2477
+ }
2478
+ /**
2479
+ * Main translation: Google Generative AI request → OpenAI ChatCompletions payload.
2480
+ */
2481
+ function translateGoogleToOpenAI(googlePayload, model, stream) {
2482
+ const messages = [];
2483
+ const systemMessage = translateSystemInstruction(googlePayload.systemInstruction);
2484
+ if (systemMessage) messages.push(systemMessage);
2485
+ messages.push(...translateContents(googlePayload.contents));
2486
+ return {
2487
+ model,
2488
+ messages,
2489
+ ...translateGenerationConfig(googlePayload.generationConfig, stream),
2490
+ tools: translateTools(googlePayload.tools),
2491
+ tool_choice: translateToolChoice(googlePayload.toolConfig)
2492
+ };
2493
+ }
2494
+
2495
+ //#endregion
2496
+ //#region src/routes/google-ai/response-translation.ts
2497
+ function mapFinishReason(reason) {
2498
+ switch (reason) {
2499
+ case "stop": return "STOP";
2500
+ case "length": return "MAX_TOKENS";
2501
+ case "content_filter": return "SAFETY";
2502
+ case "tool_calls": return "STOP";
2503
+ default: return null;
2504
+ }
2505
+ }
2506
+ function translateUsage(usage) {
2507
+ if (!usage) return void 0;
2508
+ return {
2509
+ promptTokenCount: usage.prompt_tokens,
2510
+ candidatesTokenCount: usage.completion_tokens,
2511
+ totalTokenCount: usage.total_tokens,
2512
+ cachedContentTokenCount: usage.prompt_tokens_details?.cached_tokens ?? void 0
2513
+ };
2514
+ }
2515
+ /**
2516
+ * Parse tool call arguments from JSON string, with fallback to raw string.
2517
+ */
2518
+ function parseToolCallArgs(argsString) {
2519
+ try {
2520
+ return JSON.parse(argsString);
2521
+ } catch {
2522
+ return { raw: argsString };
2523
+ }
2524
+ }
2525
+ /**
2526
+ * Convert OpenAI ChatCompletion response → Google Generative AI response.
2527
+ */
2528
+ function translateOpenAIToGoogle(response) {
2529
+ return {
2530
+ candidates: response.choices.map((choice) => {
2531
+ const parts = [];
2532
+ if (choice.message.content) parts.push({ text: choice.message.content });
2533
+ if (choice.message.tool_calls) for (const toolCall of choice.message.tool_calls) parts.push({ functionCall: {
2534
+ name: toolCall.function.name,
2535
+ args: parseToolCallArgs(toolCall.function.arguments)
2536
+ } });
2537
+ if (parts.length === 0) parts.push({ text: "" });
2538
+ return {
2539
+ content: {
2540
+ role: "model",
2541
+ parts
2542
+ },
2543
+ finishReason: mapFinishReason(choice.finish_reason),
2544
+ index: choice.index
2545
+ };
2546
+ }),
2547
+ usageMetadata: translateUsage(response.usage)
2548
+ };
2549
+ }
2550
+ function createGoogleStreamState() {
2551
+ return {
2552
+ toolCalls: /* @__PURE__ */ new Map(),
2553
+ hasContent: false
2554
+ };
2555
+ }
2556
+ /**
2557
+ * Accumulate incremental tool call deltas into the stream state.
2558
+ */
2559
+ function accumulateToolCallDeltas(toolCallDeltas, streamState) {
2560
+ for (const tc of toolCallDeltas) {
2561
+ const existing = streamState.toolCalls.get(tc.index);
2562
+ if (existing) {
2563
+ if (tc.function?.arguments) existing.arguments += tc.function.arguments;
2564
+ } else streamState.toolCalls.set(tc.index, {
2565
+ name: tc.function?.name ?? "",
2566
+ arguments: tc.function?.arguments ?? ""
2567
+ });
2568
+ }
2569
+ }
2570
+ /**
2571
+ * Emit accumulated tool calls as Google functionCall parts and clear state.
2572
+ */
2573
+ function emitAccumulatedToolCalls(streamState) {
2574
+ const parts = [];
2575
+ for (const [, tc] of streamState.toolCalls) parts.push({ functionCall: {
2576
+ name: tc.name,
2577
+ args: parseToolCallArgs(tc.arguments)
2578
+ } });
2579
+ streamState.toolCalls.clear();
2580
+ return parts;
2581
+ }
2582
+ /**
2583
+ * Build a Google streaming chunk from parts and optional finish/usage info.
2584
+ */
2585
+ function buildStreamChunk(options) {
2586
+ const { parts, finishReason, index, usage } = options;
2587
+ if (parts.length === 0 && finishReason) parts.push({ text: "" });
2588
+ return {
2589
+ candidates: [{
2590
+ content: {
2591
+ role: "model",
2592
+ parts
2593
+ },
2594
+ finishReason: finishReason ? mapFinishReason(finishReason) : null,
2595
+ index
2596
+ }],
2597
+ usageMetadata: usage ? translateUsage(usage) : void 0
2598
+ };
2599
+ }
2600
+ /**
2601
+ * Translate a single OpenAI streaming chunk → Google streaming chunk.
2602
+ * Returns null if the chunk doesn't produce a Google event.
2603
+ */
2604
+ function translateChunkToGoogle(chunk, streamState) {
2605
+ if (chunk.choices.length === 0) {
2606
+ if (chunk.usage) return {
2607
+ candidates: [],
2608
+ usageMetadata: translateUsage(chunk.usage)
2609
+ };
2610
+ return null;
2611
+ }
2612
+ const choice = chunk.choices[0];
2613
+ const parts = [];
2614
+ if (choice.delta.content !== null && choice.delta.content !== void 0) {
2615
+ parts.push({ text: choice.delta.content });
2616
+ streamState.hasContent = true;
2617
+ }
2618
+ if (choice.delta.tool_calls) accumulateToolCallDeltas(choice.delta.tool_calls, streamState);
2619
+ if (choice.finish_reason === "tool_calls") parts.push(...emitAccumulatedToolCalls(streamState));
2620
+ if (parts.length === 0 && !choice.finish_reason) return null;
2621
+ return buildStreamChunk({
2622
+ parts,
2623
+ finishReason: choice.finish_reason,
2624
+ index: choice.index,
2625
+ usage: chunk.usage
2626
+ });
2627
+ }
2628
+ /**
2629
+ * Map Responses API status → Google finish reason.
2630
+ */
2631
+ function mapResponsesFinishReason(status, incompleteDetails) {
2632
+ if (status === "completed") return "STOP";
2633
+ if (status === "incomplete") {
2634
+ if (incompleteDetails?.reason === "max_output_tokens") return "MAX_TOKENS";
2635
+ if (incompleteDetails?.reason === "content_filter") return "SAFETY";
2636
+ return "MAX_TOKENS";
2637
+ }
2638
+ if (status === "failed") return "OTHER";
2639
+ return null;
2640
+ }
2641
+ /**
2642
+ * Translate Responses API usage → Google usage metadata.
2643
+ */
2644
+ function translateResponsesUsage(usage) {
2645
+ if (!usage) return void 0;
2646
+ return {
2647
+ promptTokenCount: usage.input_tokens,
2648
+ candidatesTokenCount: usage.output_tokens,
2649
+ totalTokenCount: usage.total_tokens,
2650
+ cachedContentTokenCount: usage.input_tokens_details?.cached_tokens ?? void 0
2651
+ };
2652
+ }
2653
+ /**
2654
+ * Type guard for ResponseOutputText blocks.
2655
+ */
2656
+ function isOutputTextBlock(block) {
2657
+ return typeof block === "object" && block !== null && "type" in block && block.type === "output_text";
2658
+ }
2659
+ /**
2660
+ * Convert Responses API result → Google Generative AI response (non-streaming).
2661
+ */
2662
+ function translateResponsesResultToGoogle(result) {
2663
+ const parts = [];
2664
+ for (const item of result.output) if (item.type === "message" && item.content) {
2665
+ for (const block of item.content) if (isOutputTextBlock(block)) parts.push({ text: block.text });
2666
+ } else if (item.type === "function_call") {
2667
+ const funcCall = item;
2668
+ parts.push({ functionCall: {
2669
+ name: funcCall.name,
2670
+ args: parseToolCallArgs(funcCall.arguments)
2671
+ } });
2672
+ }
2673
+ if (parts.length === 0) parts.push({ text: result.output_text || "" });
2674
+ const finishReason = mapResponsesFinishReason(result.status, result.incomplete_details);
2675
+ return {
2676
+ candidates: [{
2677
+ content: {
2678
+ role: "model",
2679
+ parts
2680
+ },
2681
+ finishReason,
2682
+ index: 0
2683
+ }],
2684
+ usageMetadata: translateResponsesUsage(result.usage)
2685
+ };
2686
+ }
2687
+ /**
2688
+ * Translate a single Responses API stream event → Google streaming chunk.
2689
+ * Returns null if the event doesn't produce a Google event.
2690
+ */
2691
+ function translateResponsesStreamEventToGoogle(event, _streamState) {
2692
+ switch (event.type) {
2693
+ case "response.output_text.delta": return { candidates: [{
2694
+ content: {
2695
+ role: "model",
2696
+ parts: [{ text: event.delta }]
2697
+ },
2698
+ finishReason: null,
2699
+ index: 0
2700
+ }] };
2701
+ case "response.function_call_arguments.done": return { candidates: [{
2702
+ content: {
2703
+ role: "model",
2704
+ parts: [{ functionCall: {
2705
+ name: event.name,
2706
+ args: parseToolCallArgs(event.arguments)
2707
+ } }]
2708
+ },
2709
+ finishReason: null,
2710
+ index: 0
2711
+ }] };
2712
+ case "response.completed":
2713
+ case "response.incomplete": return {
2714
+ candidates: [{
2715
+ content: {
2716
+ role: "model",
2717
+ parts: [{ text: "" }]
2718
+ },
2719
+ finishReason: mapResponsesFinishReason(event.response.status, event.response.incomplete_details),
2720
+ index: 0
2721
+ }],
2722
+ usageMetadata: translateResponsesUsage(event.response.usage)
2723
+ };
2724
+ default: return null;
2725
+ }
2726
+ }
2727
+
2728
+ //#endregion
2729
+ //#region src/routes/google-ai/handler.ts
2730
+ const logger$2 = createHandlerLogger("google-ai-handler");
2731
+ const RESPONSES_ENDPOINT$2 = "/responses";
2732
+ /**
2733
+ * Parse model name and action from the URL path segment.
2734
+ * e.g. "gemini-3-flash-preview:streamGenerateContent" → { model: "gemini-3-flash-preview", action: "streamGenerateContent" }
2735
+ */
2736
+ function parseModelAction(modelAction) {
2737
+ const colonIdx = modelAction.lastIndexOf(":");
2738
+ if (colonIdx === -1) return {
2739
+ model: modelAction,
2740
+ action: "generateContent"
2741
+ };
2742
+ return {
2743
+ model: modelAction.slice(0, colonIdx),
2744
+ action: modelAction.slice(colonIdx + 1)
2745
+ };
2746
+ }
2747
+ /**
2748
+ * Cap max_tokens at the model's advertised limit to prevent 400 errors.
2749
+ */
2750
+ function capMaxTokens(payload, selectedModel) {
2751
+ const maxAllowed = selectedModel?.capabilities.limits.max_output_tokens;
2752
+ if (!maxAllowed) return;
2753
+ if (isNullish(payload.max_tokens)) payload.max_tokens = maxAllowed;
2754
+ else if (payload.max_tokens > maxAllowed) {
2755
+ consola.debug(`Capping max_tokens from ${payload.max_tokens} to ${maxAllowed} for ${payload.model}`);
2756
+ payload.max_tokens = maxAllowed;
2757
+ }
2758
+ }
2759
+ async function handleGoogleAI(c) {
2760
+ await checkRateLimit(state);
2761
+ const modelAction = c.req.param("modelAction");
2762
+ if (!modelAction) return c.json({ error: {
2763
+ code: 400,
2764
+ message: "Missing model and action in URL path",
2765
+ status: "INVALID_ARGUMENT"
2766
+ } }, 400);
2767
+ const { model: rawModel, action } = parseModelAction(modelAction);
2768
+ const isStream = action === "streamGenerateContent";
2769
+ const model = normalizeModelName(rawModel);
2770
+ logger$2.debug(`Google AI request: model=${model}, action=${action}`);
2771
+ const googlePayload = await c.req.json();
2772
+ logger$2.debug("Google AI request payload:", JSON.stringify(googlePayload));
2773
+ const openAIPayload = translateGoogleToOpenAI(googlePayload, model, isStream);
2774
+ const { payload: replacedPayload, appliedRules } = await applyReplacementsToPayload(openAIPayload);
2775
+ const finalPayload = {
2776
+ ...replacedPayload,
2777
+ model: normalizeModelName(replacedPayload.model)
2778
+ };
2779
+ const selectedModel = state.models?.data.find((m) => m.id === finalPayload.model);
2780
+ const useResponsesApi = selectedModel?.supported_endpoints?.includes(RESPONSES_ENDPOINT$2) ?? false;
2781
+ setRequestContext(c, {
2782
+ requestedModel: rawModel,
2783
+ model: finalPayload.model,
2784
+ provider: useResponsesApi ? "GoogleAI→Responses" : "GoogleAI→ChatCompletions",
2785
+ replacements: appliedRules
2041
2786
  });
2042
- const selectedModel = state.models?.data.find((model) => model.id === payload.model);
2043
2787
  try {
2044
2788
  if (selectedModel) {
2045
- const tokenCount = await getTokenCount(payload, selectedModel);
2789
+ const tokenCount = await getTokenCount(finalPayload, selectedModel);
2046
2790
  setRequestContext(c, { inputTokens: tokenCount.input });
2047
2791
  }
2048
- } catch (error) {
2049
- consola.warn("Failed to calculate token count:", error);
2050
- }
2792
+ } catch {}
2051
2793
  if (state.manualApprove) await awaitApproval();
2052
- if (isNullish(payload.max_tokens)) {
2053
- payload = {
2054
- ...payload,
2055
- max_tokens: selectedModel?.capabilities.limits.max_output_tokens
2056
- };
2057
- consola.debug("Set max_tokens to:", JSON.stringify(payload.max_tokens));
2058
- }
2059
- const response = await createChatCompletions(payload);
2060
- if (isNonStreaming$1(response)) {
2061
- consola.debug("Non-streaming response:", JSON.stringify(response));
2794
+ capMaxTokens(finalPayload, selectedModel);
2795
+ consola.debug(`[google-ai] Translated payload: model=${finalPayload.model}, max_tokens=${finalPayload.max_tokens}, stream=${finalPayload.stream}, tools=${finalPayload.tools?.length ?? 0}, messages=${finalPayload.messages.length}`);
2796
+ logger$2.debug("Translated OpenAI payload:", JSON.stringify(finalPayload));
2797
+ if (useResponsesApi) {
2798
+ consola.debug(`[google-ai] Using Responses API for ${finalPayload.model}`);
2799
+ return handleWithResponsesApi$1(c, finalPayload, isStream);
2800
+ }
2801
+ consola.debug(`[google-ai] Using ChatCompletions API for ${finalPayload.model}`);
2802
+ return handleWithChatCompletions$1(c, finalPayload);
2803
+ }
2804
+ async function handleWithChatCompletions$1(c, finalPayload) {
2805
+ const response = await createChatCompletions(finalPayload);
2806
+ if (isNonStreamingCC(response)) {
2807
+ logger$2.debug("Non-streaming response from Copilot:", JSON.stringify(response));
2062
2808
  if (response.usage) setRequestContext(c, {
2063
2809
  inputTokens: response.usage.prompt_tokens,
2064
2810
  outputTokens: response.usage.completion_tokens
2065
2811
  });
2066
- return c.json(response);
2812
+ const googleResponse = translateOpenAIToGoogle(response);
2813
+ return c.json(googleResponse);
2067
2814
  }
2068
- consola.debug("Streaming response");
2815
+ logger$2.debug("Streaming response from Copilot");
2069
2816
  return streamSSE(c, async (stream) => {
2070
- for await (const chunk of response) {
2071
- consola.debug("Streaming chunk:", JSON.stringify(chunk));
2072
- if (chunk.data && chunk.data !== "[DONE]") {
2073
- const parsed = JSON.parse(chunk.data);
2074
- if (parsed.usage) setRequestContext(c, {
2075
- inputTokens: parsed.usage.prompt_tokens,
2076
- outputTokens: parsed.usage.completion_tokens
2077
- });
2078
- }
2079
- await stream.writeSSE(chunk);
2817
+ const streamState = createGoogleStreamState();
2818
+ for await (const rawEvent of response) {
2819
+ logger$2.debug("Copilot raw stream event:", JSON.stringify(rawEvent));
2820
+ if (rawEvent.data === "[DONE]") break;
2821
+ if (!rawEvent.data) continue;
2822
+ const chunk = JSON.parse(rawEvent.data);
2823
+ if (chunk.usage) setRequestContext(c, {
2824
+ inputTokens: chunk.usage.prompt_tokens,
2825
+ outputTokens: chunk.usage.completion_tokens
2826
+ });
2827
+ const googleChunk = translateChunkToGoogle(chunk, streamState);
2828
+ if (googleChunk) await stream.writeSSE({ data: JSON.stringify(googleChunk) });
2080
2829
  }
2081
2830
  });
2082
2831
  }
2083
- const isNonStreaming$1 = (response) => Object.hasOwn(response, "choices");
2084
-
2085
- //#endregion
2086
- //#region src/routes/chat-completions/route.ts
2087
- const completionRoutes = new Hono();
2088
- completionRoutes.post("/", async (c) => {
2089
- try {
2090
- return await handleCompletion$1(c);
2091
- } catch (error) {
2092
- return await forwardError(c, error);
2832
+ /**
2833
+ * Convert an OpenAI ChatCompletions payload to a Responses API payload.
2834
+ */
2835
+ function openAIPayloadToResponses(payload) {
2836
+ const instructions = payload.messages.filter((m) => m.role === "system").map((m) => typeof m.content === "string" ? m.content : "").join("\n") || null;
2837
+ const input = convertMessagesToInput(payload.messages);
2838
+ const tools = convertToolsToResponses(payload.tools);
2839
+ const toolChoice = convertToolChoiceToResponses(payload.tool_choice);
2840
+ return {
2841
+ model: payload.model,
2842
+ input,
2843
+ instructions,
2844
+ temperature: payload.temperature ?? null,
2845
+ top_p: payload.top_p ?? null,
2846
+ max_output_tokens: payload.max_tokens ?? null,
2847
+ tools,
2848
+ tool_choice: toolChoice,
2849
+ stream: payload.stream ?? null,
2850
+ store: false,
2851
+ parallel_tool_calls: true
2852
+ };
2853
+ }
2854
+ /**
2855
+ * Convert OpenAI messages to Responses API input items.
2856
+ */
2857
+ function convertMessagesToInput(messages) {
2858
+ const input = [];
2859
+ for (const msg of messages) {
2860
+ if (msg.role === "system") continue;
2861
+ switch (msg.role) {
2862
+ case "user":
2863
+ input.push(createResponseMessage("user", typeof msg.content === "string" ? msg.content : ""));
2864
+ break;
2865
+ case "assistant":
2866
+ if (msg.content) input.push(createResponseMessage("assistant", typeof msg.content === "string" ? msg.content : ""));
2867
+ if (msg.tool_calls) for (const tc of msg.tool_calls) input.push({
2868
+ type: "function_call",
2869
+ call_id: tc.id,
2870
+ name: tc.function.name,
2871
+ arguments: tc.function.arguments,
2872
+ status: "completed"
2873
+ });
2874
+ break;
2875
+ case "tool":
2876
+ input.push({
2877
+ type: "function_call_output",
2878
+ call_id: msg.tool_call_id ?? "",
2879
+ output: typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content)
2880
+ });
2881
+ break;
2882
+ }
2093
2883
  }
2094
- });
2095
-
2096
- //#endregion
2097
- //#region src/services/copilot/create-embeddings.ts
2098
- const createEmbeddings = async (payload) => {
2099
- if (!state.copilotToken) throw new Error("Copilot token not found");
2100
- const response = await fetchWithRetry(`${copilotBaseUrl(state)}/embeddings`, {
2101
- method: "POST",
2102
- headers: copilotHeaders(state),
2103
- body: JSON.stringify(payload)
2884
+ return input;
2885
+ }
2886
+ /**
2887
+ * Convert OpenAI tools to Responses API tools.
2888
+ */
2889
+ function convertToolsToResponses(tools) {
2890
+ return tools?.map((t) => ({
2891
+ type: "function",
2892
+ name: t.function.name,
2893
+ description: t.function.description ?? null,
2894
+ parameters: t.function.parameters,
2895
+ strict: false
2896
+ })) ?? null;
2897
+ }
2898
+ /**
2899
+ * Convert OpenAI tool_choice to Responses API tool_choice.
2900
+ */
2901
+ function convertToolChoiceToResponses(toolChoice) {
2902
+ if (typeof toolChoice === "string") return toolChoice;
2903
+ if (toolChoice && typeof toolChoice === "object" && "function" in toolChoice) return {
2904
+ type: "function",
2905
+ name: toolChoice.function.name
2906
+ };
2907
+ return "auto";
2908
+ }
2909
+ function createResponseMessage(role, content) {
2910
+ return {
2911
+ type: "message",
2912
+ role,
2913
+ content
2914
+ };
2915
+ }
2916
+ async function handleWithResponsesApi$1(c, payload, isStream) {
2917
+ const responsesPayload = openAIPayloadToResponses(payload);
2918
+ logger$2.debug("Translated Responses payload:", JSON.stringify(responsesPayload));
2919
+ const response = await createResponses(responsesPayload, {
2920
+ vision: false,
2921
+ initiator: "user"
2104
2922
  });
2105
- if (!response.ok) throw new HTTPError("Failed to create embeddings", response);
2106
- return await response.json();
2107
- };
2923
+ if (!isStream || !isAsyncIterable$2(response)) {
2924
+ const result = response;
2925
+ logger$2.debug("Non-streaming Responses result:", JSON.stringify(result).slice(-400));
2926
+ if (result.usage) setRequestContext(c, {
2927
+ inputTokens: result.usage.input_tokens,
2928
+ outputTokens: result.usage.output_tokens
2929
+ });
2930
+ const googleResponse = translateResponsesResultToGoogle(result);
2931
+ return c.json(googleResponse);
2932
+ }
2933
+ logger$2.debug("Streaming response from Copilot (Responses API)");
2934
+ return streamSSE(c, async (stream) => {
2935
+ const streamState = createGoogleStreamState();
2936
+ for await (const chunk of response) {
2937
+ if (chunk.event === "ping") continue;
2938
+ const data = chunk.data;
2939
+ if (!data) continue;
2940
+ logger$2.debug("Responses raw stream event:", data);
2941
+ const parsed = JSON.parse(data);
2942
+ const googleChunk = translateResponsesStreamEventToGoogle(parsed, streamState);
2943
+ if (googleChunk) {
2944
+ if (parsed.type === "response.completed" || parsed.type === "response.incomplete") {
2945
+ const usage = parsed.response.usage;
2946
+ if (usage) setRequestContext(c, {
2947
+ inputTokens: usage.input_tokens,
2948
+ outputTokens: usage.output_tokens
2949
+ });
2950
+ }
2951
+ await stream.writeSSE({ data: JSON.stringify(googleChunk) });
2952
+ }
2953
+ }
2954
+ });
2955
+ }
2956
+ const isNonStreamingCC = (response) => Object.hasOwn(response, "choices");
2957
+ const isAsyncIterable$2 = (value) => Boolean(value) && typeof value[Symbol.asyncIterator] === "function";
2108
2958
 
2109
2959
  //#endregion
2110
- //#region src/routes/embeddings/route.ts
2111
- const embeddingRoutes = new Hono();
2112
- embeddingRoutes.post("/", async (c) => {
2960
+ //#region src/routes/google-ai/route.ts
2961
+ const googleAIRoutes = new Hono();
2962
+ googleAIRoutes.post("/:modelAction", async (c) => {
2113
2963
  try {
2114
- const paylod = await c.req.json();
2115
- const response = await createEmbeddings(paylod);
2116
- return c.json(response);
2964
+ return await handleGoogleAI(c);
2117
2965
  } catch (error) {
2118
2966
  return await forwardError(c, error);
2119
2967
  }
@@ -2363,141 +3211,6 @@ async function handleCountTokens(c) {
2363
3211
  }
2364
3212
  }
2365
3213
 
2366
- //#endregion
2367
- //#region src/lib/logger.ts
2368
- const LOG_RETENTION_MS = 10080 * 60 * 1e3;
2369
- const CLEANUP_INTERVAL_MS = 1440 * 60 * 1e3;
2370
- const LOG_DIR = path.join(PATHS.APP_DIR, "logs");
2371
- const FLUSH_INTERVAL_MS = 1e3;
2372
- const MAX_BUFFER_SIZE = 100;
2373
- const logStreams = /* @__PURE__ */ new Map();
2374
- const logBuffers = /* @__PURE__ */ new Map();
2375
- const ensureLogDirectory = () => {
2376
- if (!fs$1.existsSync(LOG_DIR)) fs$1.mkdirSync(LOG_DIR, { recursive: true });
2377
- };
2378
- const cleanupOldLogs = () => {
2379
- if (!fs$1.existsSync(LOG_DIR)) return;
2380
- const now = Date.now();
2381
- for (const entry of fs$1.readdirSync(LOG_DIR)) {
2382
- const filePath = path.join(LOG_DIR, entry);
2383
- let stats;
2384
- try {
2385
- stats = fs$1.statSync(filePath);
2386
- } catch {
2387
- continue;
2388
- }
2389
- if (!stats.isFile()) continue;
2390
- if (now - stats.mtimeMs > LOG_RETENTION_MS) try {
2391
- fs$1.rmSync(filePath);
2392
- } catch {
2393
- continue;
2394
- }
2395
- }
2396
- };
2397
- const formatArgs = (args) => args.map((arg) => typeof arg === "string" ? arg : util.inspect(arg, {
2398
- depth: null,
2399
- colors: false
2400
- })).join(" ");
2401
- const sanitizeName = (name$1) => {
2402
- const normalized = name$1.toLowerCase().replaceAll(/[^a-z0-9]+/g, "-").replaceAll(/^-+|-+$/g, "");
2403
- return normalized === "" ? "handler" : normalized;
2404
- };
2405
- const getLogStream = (filePath) => {
2406
- let stream = logStreams.get(filePath);
2407
- if (!stream || stream.destroyed) {
2408
- stream = fs$1.createWriteStream(filePath, { flags: "a" });
2409
- logStreams.set(filePath, stream);
2410
- stream.on("error", (error) => {
2411
- console.warn("Log stream error", error);
2412
- logStreams.delete(filePath);
2413
- });
2414
- }
2415
- return stream;
2416
- };
2417
- const flushBuffer = (filePath) => {
2418
- const buffer = logBuffers.get(filePath);
2419
- if (!buffer || buffer.length === 0) return;
2420
- const stream = getLogStream(filePath);
2421
- const content = buffer.join("\n") + "\n";
2422
- stream.write(content, (error) => {
2423
- if (error) console.warn("Failed to write handler log", error);
2424
- });
2425
- logBuffers.set(filePath, []);
2426
- };
2427
- const flushAllBuffers = () => {
2428
- for (const filePath of logBuffers.keys()) flushBuffer(filePath);
2429
- };
2430
- const appendLine = (filePath, line) => {
2431
- let buffer = logBuffers.get(filePath);
2432
- if (!buffer) {
2433
- buffer = [];
2434
- logBuffers.set(filePath, buffer);
2435
- }
2436
- buffer.push(line);
2437
- if (buffer.length >= MAX_BUFFER_SIZE) flushBuffer(filePath);
2438
- };
2439
- setInterval(flushAllBuffers, FLUSH_INTERVAL_MS);
2440
- const cleanup = () => {
2441
- flushAllBuffers();
2442
- for (const stream of logStreams.values()) stream.end();
2443
- logStreams.clear();
2444
- logBuffers.clear();
2445
- };
2446
- process.on("exit", cleanup);
2447
- process.on("SIGINT", () => {
2448
- cleanup();
2449
- process.exit(0);
2450
- });
2451
- process.on("SIGTERM", () => {
2452
- cleanup();
2453
- process.exit(0);
2454
- });
2455
- let lastCleanup = 0;
2456
- const createHandlerLogger = (name$1) => {
2457
- ensureLogDirectory();
2458
- const sanitizedName = sanitizeName(name$1);
2459
- const instance = consola.withTag(name$1);
2460
- if (state.verbose) instance.level = 5;
2461
- instance.setReporters([]);
2462
- instance.addReporter({ log(logObj) {
2463
- ensureLogDirectory();
2464
- if (Date.now() - lastCleanup > CLEANUP_INTERVAL_MS) {
2465
- cleanupOldLogs();
2466
- lastCleanup = Date.now();
2467
- }
2468
- const date = logObj.date;
2469
- const dateKey = date.toLocaleDateString("sv-SE");
2470
- const timestamp = date.toLocaleString("sv-SE", { hour12: false });
2471
- const filePath = path.join(LOG_DIR, `${sanitizedName}-${dateKey}.log`);
2472
- const message = formatArgs(logObj.args);
2473
- const line = `[${timestamp}] [${logObj.type}] [${logObj.tag || name$1}]${message ? ` ${message}` : ""}`;
2474
- appendLine(filePath, line);
2475
- } });
2476
- return instance;
2477
- };
2478
-
2479
- //#endregion
2480
- //#region src/services/copilot/create-responses.ts
2481
- const createResponses = async (payload, { vision, initiator }) => {
2482
- if (!state.copilotToken) throw new Error("Copilot token not found");
2483
- const headers = {
2484
- ...copilotHeaders(state, vision),
2485
- "X-Initiator": initiator
2486
- };
2487
- payload.service_tier = null;
2488
- const response = await fetch(`${copilotBaseUrl(state)}/responses`, {
2489
- method: "POST",
2490
- headers,
2491
- body: JSON.stringify(payload)
2492
- });
2493
- if (!response.ok) {
2494
- consola.error("Failed to create responses", response);
2495
- throw new HTTPError("Failed to create responses", response);
2496
- }
2497
- if (payload.stream) return events(response);
2498
- return await response.json();
2499
- };
2500
-
2501
3214
  //#endregion
2502
3215
  //#region src/routes/messages/responses-translation.ts
2503
3216
  const MESSAGE_TYPE = "message";
@@ -4046,21 +4759,6 @@ responsesRoutes.post("/", async (c) => {
4046
4759
  }
4047
4760
  });
4048
4761
 
4049
- //#endregion
4050
- //#region src/routes/token/route.ts
4051
- const tokenRoute = new Hono();
4052
- tokenRoute.get("/", (c) => {
4053
- try {
4054
- return c.json({ token: state.copilotToken });
4055
- } catch (error) {
4056
- console.error("Error fetching token:", error);
4057
- return c.json({
4058
- error: "Failed to fetch token",
4059
- token: null
4060
- }, 500);
4061
- }
4062
- });
4063
-
4064
4762
  //#endregion
4065
4763
  //#region src/routes/usage/route.ts
4066
4764
  const usageRoute = new Hono();
@@ -4086,7 +4784,6 @@ server.route("/chat/completions", completionRoutes);
4086
4784
  server.route("/models", modelRoutes);
4087
4785
  server.route("/embeddings", embeddingRoutes);
4088
4786
  server.route("/usage", usageRoute);
4089
- server.route("/token", tokenRoute);
4090
4787
  server.route("/replacements", replacementsRoute);
4091
4788
  server.route("/responses", responsesRoutes);
4092
4789
  server.route("/v1/chat/completions", completionRoutes);
@@ -4094,6 +4791,8 @@ server.route("/v1/models", modelRoutes);
4094
4791
  server.route("/v1/embeddings", embeddingRoutes);
4095
4792
  server.route("/v1/responses", responsesRoutes);
4096
4793
  server.route("/v1/messages", messageRoutes);
4794
+ server.route("/v1/models", googleAIRoutes);
4795
+ server.route("/models", googleAIRoutes);
4097
4796
 
4098
4797
  //#endregion
4099
4798
  //#region src/start.ts