@dianshuv/copilot-api 0.6.0 → 0.6.1

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.
Files changed (3) hide show
  1. package/README.md +26 -0
  2. package/dist/main.mjs +467 -2
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -15,6 +15,7 @@
15
15
  - **Graceful shutdown**: 4-phase shutdown sequence — stops accepting requests, waits for in-flight requests to complete, sends abort signal, then force-closes. Configurable via `--shutdown-graceful-wait` and `--shutdown-abort-wait`.
16
16
  - **Stream repetition detection**: Detects when models get stuck in repetitive output loops using KMP-based pattern matching and logs a warning.
17
17
  - **Stale request reaping**: Automatically force-fails requests that exceed a configurable maximum age (default 600s) to prevent resource leaks.
18
+ - **Gemini API compatibility**: `/v1beta/models` endpoints translate Gemini API requests to OpenAI format for Copilot. Enables Google Gemini CLI to use Copilot models via `GOOGLE_GEMINI_BASE_URL` environment variable.
18
19
  - **PostHog analytics**: Optional PostHog Cloud integration (`--posthog-key`) sends per-request token usage events for long-term trend analysis. Free tier (1M events/month) is more than sufficient for individual use.
19
20
 
20
21
  ## Quick Start
@@ -97,6 +98,14 @@ copilot-api start
97
98
  | `/v1/messages/count_tokens` | POST | Token counting |
98
99
  | `/v1/event_logging/batch` | POST | Event logging (no-op) |
99
100
 
101
+ ### Gemini Compatible
102
+
103
+ | Endpoint | Method | Description |
104
+ |----------|--------|-------------|
105
+ | `/v1beta/models/{model}:generateContent` | POST | Non-streaming generation |
106
+ | `/v1beta/models/{model}:streamGenerateContent` | POST | Streaming generation (SSE) |
107
+ | `/v1beta/models/{model}:countTokens` | POST | Token counting |
108
+
100
109
  ### Utility
101
110
 
102
111
  | Endpoint | Method | Description |
@@ -143,6 +152,23 @@ Or use the interactive setup:
143
152
  bun run start --claude-code
144
153
  ```
145
154
 
155
+ ## Using with Gemini CLI
156
+
157
+ ```bash
158
+ # Start the proxy
159
+ copilot-api start
160
+
161
+ # Configure Gemini CLI to use the proxy
162
+ export GEMINI_API_KEY="placeholder"
163
+ export GOOGLE_GEMINI_BASE_URL="http://localhost:4141"
164
+
165
+ # Basic conversation
166
+ gemini -p "Explain this code"
167
+
168
+ # Pipe review
169
+ git diff HEAD~1 | gemini -p "Review this diff for bugs"
170
+ ```
171
+
146
172
  ## Upstream Project
147
173
 
148
174
  For the original project documentation, features, and updates, see: [ericc-ch/copilot-api](https://github.com/ericc-ch/copilot-api)
package/dist/main.mjs CHANGED
@@ -17,7 +17,7 @@ import process$1 from "node:process";
17
17
  import pc from "picocolors";
18
18
  import { Hono } from "hono";
19
19
  import { cors } from "hono/cors";
20
- import { streamSSE } from "hono/streaming";
20
+ import { stream, streamSSE } from "hono/streaming";
21
21
  import { events } from "fetch-event-stream";
22
22
 
23
23
  //#region src/lib/paths.ts
@@ -1036,7 +1036,7 @@ const patchClaude = defineCommand({
1036
1036
 
1037
1037
  //#endregion
1038
1038
  //#region package.json
1039
- var version = "0.6.0";
1039
+ var version = "0.6.1";
1040
1040
 
1041
1041
  //#endregion
1042
1042
  //#region src/lib/adaptive-rate-limiter.ts
@@ -4036,6 +4036,470 @@ eventLoggingRoutes.post("/batch", (c) => {
4036
4036
  return c.text("OK", 200);
4037
4037
  });
4038
4038
 
4039
+ //#endregion
4040
+ //#region src/routes/gemini/error.ts
4041
+ const STATUS_MAP = {
4042
+ 400: "INVALID_ARGUMENT",
4043
+ 401: "PERMISSION_DENIED",
4044
+ 403: "PERMISSION_DENIED",
4045
+ 404: "NOT_FOUND",
4046
+ 413: "INVALID_ARGUMENT",
4047
+ 429: "RESOURCE_EXHAUSTED",
4048
+ 500: "INTERNAL"
4049
+ };
4050
+ function geminiError(c, code, status, message) {
4051
+ return c.json({ error: {
4052
+ code,
4053
+ message,
4054
+ status
4055
+ } }, code);
4056
+ }
4057
+ function forwardGeminiError(c, error) {
4058
+ if (error instanceof HTTPError) {
4059
+ const status = STATUS_MAP[error.status] ?? "INTERNAL";
4060
+ const code = error.status;
4061
+ let message = error.responseText;
4062
+ try {
4063
+ const parsed = JSON.parse(error.responseText);
4064
+ if (parsed.error?.message) message = parsed.error.message;
4065
+ } catch {}
4066
+ consola.error(`HTTP ${code}:`, message.slice(0, 200));
4067
+ return geminiError(c, code, status, message);
4068
+ }
4069
+ consola.error("Unexpected error:", error);
4070
+ return geminiError(c, 500, "INTERNAL", error instanceof Error ? error.message : "Unknown error");
4071
+ }
4072
+
4073
+ //#endregion
4074
+ //#region src/routes/gemini/gemini-to-openai.ts
4075
+ function translateGeminiToOpenAI(request, model) {
4076
+ const messages = [];
4077
+ if (request.systemInstruction) {
4078
+ const systemText = extractTextFromParts(request.systemInstruction.parts);
4079
+ if (systemText) messages.push({
4080
+ role: "system",
4081
+ content: systemText
4082
+ });
4083
+ }
4084
+ let globalCallIndex = 0;
4085
+ const callIdQueue = /* @__PURE__ */ new Map();
4086
+ if (!Array.isArray(request.contents)) return { payload: {
4087
+ messages: [],
4088
+ model
4089
+ } };
4090
+ for (const content of request.contents) {
4091
+ const translated = translateContent(content, callIdQueue, () => `call_gemini_${globalCallIndex++}`);
4092
+ messages.push(...translated);
4093
+ }
4094
+ const payload = {
4095
+ messages,
4096
+ model
4097
+ };
4098
+ const config = request.generationConfig;
4099
+ if (config) {
4100
+ if (config.temperature !== void 0) payload.temperature = config.temperature;
4101
+ if (config.topP !== void 0) payload.top_p = config.topP;
4102
+ if (config.maxOutputTokens !== void 0) payload.max_tokens = config.maxOutputTokens;
4103
+ if (config.stopSequences !== void 0) payload.stop = config.stopSequences;
4104
+ if (config.responseMimeType === "application/json") payload.response_format = { type: "json_object" };
4105
+ }
4106
+ if (request.tools) {
4107
+ const tools = translateTools(request.tools);
4108
+ if (tools.length > 0) payload.tools = tools;
4109
+ }
4110
+ if (request.toolConfig?.functionCallingConfig?.mode) payload.tool_choice = {
4111
+ AUTO: "auto",
4112
+ ANY: "required",
4113
+ NONE: "none"
4114
+ }[request.toolConfig.functionCallingConfig.mode];
4115
+ return { payload };
4116
+ }
4117
+ function mapFunctionCallsToToolCalls(functionCalls, callIdQueue, generateId) {
4118
+ return functionCalls.map((fc) => {
4119
+ const id = generateId();
4120
+ pushToQueue(callIdQueue, fc.functionCall.name, id);
4121
+ return {
4122
+ id,
4123
+ type: "function",
4124
+ function: {
4125
+ name: fc.functionCall.name,
4126
+ arguments: JSON.stringify(fc.functionCall.args)
4127
+ }
4128
+ };
4129
+ });
4130
+ }
4131
+ function translateContent(content, callIdQueue, generateId) {
4132
+ const role = content.role === "model" ? "assistant" : "user";
4133
+ const messages = [];
4134
+ const textParts = [];
4135
+ const imageParts = [];
4136
+ const functionCalls = [];
4137
+ const functionResponses = [];
4138
+ for (const part of content.parts) if (isTextPart(part)) {
4139
+ if (!part.thought) textParts.push(part);
4140
+ } else if (isInlineDataPart(part)) imageParts.push(part);
4141
+ else if (isFunctionCallPart(part)) functionCalls.push(part);
4142
+ else if (isFunctionResponsePart(part)) functionResponses.push(part);
4143
+ else if (isFileDataPart(part)) throw new HTTPError("fileData parts are not supported", 400, "fileData parts are not supported");
4144
+ if (imageParts.length > 0) {
4145
+ const contentParts = [];
4146
+ for (const part of content.parts) if (isTextPart(part) && !part.thought) contentParts.push({
4147
+ type: "text",
4148
+ text: part.text
4149
+ });
4150
+ else if (isInlineDataPart(part)) contentParts.push({
4151
+ type: "image_url",
4152
+ image_url: { url: `data:${part.inlineData.mimeType};base64,${part.inlineData.data}` }
4153
+ });
4154
+ const msg = {
4155
+ role,
4156
+ content: contentParts
4157
+ };
4158
+ if (functionCalls.length > 0 && role === "assistant") msg.tool_calls = mapFunctionCallsToToolCalls(functionCalls, callIdQueue, generateId);
4159
+ messages.push(msg);
4160
+ } else if (functionCalls.length > 0 && role === "assistant") {
4161
+ const textContent = textParts.length > 0 ? textParts.map((p) => p.text).join("") : null;
4162
+ messages.push({
4163
+ role: "assistant",
4164
+ content: textContent,
4165
+ tool_calls: mapFunctionCallsToToolCalls(functionCalls, callIdQueue, generateId)
4166
+ });
4167
+ } else if (textParts.length > 0) messages.push({
4168
+ role,
4169
+ content: textParts.map((p) => p.text).join("")
4170
+ });
4171
+ let orphanIndex = 0;
4172
+ for (const fr of functionResponses) {
4173
+ const queue = callIdQueue.get(fr.functionResponse.name);
4174
+ const id = queue && queue.length > 0 ? queue.shift() : `call_gemini_orphan_${orphanIndex++}`;
4175
+ messages.push({
4176
+ role: "tool",
4177
+ content: JSON.stringify(fr.functionResponse.response),
4178
+ tool_call_id: id
4179
+ });
4180
+ }
4181
+ return messages;
4182
+ }
4183
+ function translateTools(geminiTools) {
4184
+ const tools = [];
4185
+ for (const tool of geminiTools) if (tool.functionDeclarations) for (const decl of tool.functionDeclarations) tools.push({
4186
+ type: "function",
4187
+ function: {
4188
+ name: decl.name,
4189
+ description: decl.description,
4190
+ parameters: decl.parameters ?? {
4191
+ type: "object",
4192
+ properties: {}
4193
+ }
4194
+ }
4195
+ });
4196
+ return tools;
4197
+ }
4198
+ function pushToQueue(queue, name, id) {
4199
+ const existing = queue.get(name);
4200
+ if (existing) existing.push(id);
4201
+ else queue.set(name, [id]);
4202
+ }
4203
+ function extractTextFromParts(parts) {
4204
+ return parts.filter((p) => "text" in p && (!("thought" in p) || !p.thought)).map((p) => p.text).join("\n");
4205
+ }
4206
+ function isTextPart(part) {
4207
+ return "text" in part;
4208
+ }
4209
+ function isInlineDataPart(part) {
4210
+ return "inlineData" in part;
4211
+ }
4212
+ function isFunctionCallPart(part) {
4213
+ return "functionCall" in part;
4214
+ }
4215
+ function isFunctionResponsePart(part) {
4216
+ return "functionResponse" in part;
4217
+ }
4218
+ function isFileDataPart(part) {
4219
+ return "fileData" in part;
4220
+ }
4221
+
4222
+ //#endregion
4223
+ //#region src/routes/gemini/count-tokens-handler.ts
4224
+ async function handleGeminiCountTokens(c, model) {
4225
+ try {
4226
+ const { payload } = translateGeminiToOpenAI(await c.req.json(), model);
4227
+ const selectedModel = state.models?.data.find((m) => m.id === model);
4228
+ if (!selectedModel) {
4229
+ consola.warn("Model not found for count_tokens, returning estimate");
4230
+ return c.json({ totalTokens: 1 });
4231
+ }
4232
+ const tokenCount = await getTokenCount(payload, selectedModel);
4233
+ const totalTokens = tokenCount.input + tokenCount.output;
4234
+ consola.debug(`Gemini countTokens: ${totalTokens} tokens`);
4235
+ return c.json({ totalTokens });
4236
+ } catch (error) {
4237
+ return forwardGeminiError(c, error);
4238
+ }
4239
+ }
4240
+
4241
+ //#endregion
4242
+ //#region src/routes/gemini/openai-to-gemini.ts
4243
+ function translateOpenAIResponseToGemini(response, model) {
4244
+ const choice = response.choices.at(0);
4245
+ if (!choice) return {
4246
+ candidates: [],
4247
+ usageMetadata: buildUsageMetadata(response.usage),
4248
+ modelVersion: model
4249
+ };
4250
+ const parts = [];
4251
+ if (choice.message.content) parts.push({ text: choice.message.content });
4252
+ if (choice.message.tool_calls) for (const tc of choice.message.tool_calls) {
4253
+ const args = parseArgs(tc.function.arguments);
4254
+ parts.push({ functionCall: {
4255
+ name: tc.function.name,
4256
+ args
4257
+ } });
4258
+ }
4259
+ if (parts.length === 0) parts.push({ text: "" });
4260
+ return {
4261
+ candidates: [{
4262
+ content: {
4263
+ role: "model",
4264
+ parts
4265
+ },
4266
+ finishReason: mapFinishReason(choice.finish_reason),
4267
+ index: 0
4268
+ }],
4269
+ usageMetadata: buildUsageMetadata(response.usage),
4270
+ modelVersion: model
4271
+ };
4272
+ }
4273
+ function createGeminiStreamState() {
4274
+ return {
4275
+ toolCalls: /* @__PURE__ */ new Map(),
4276
+ usage: {
4277
+ promptTokens: 0,
4278
+ completionTokens: 0,
4279
+ totalTokens: 0
4280
+ },
4281
+ model: "",
4282
+ finishReason: ""
4283
+ };
4284
+ }
4285
+ function translateOpenAIChunkToGemini(chunk, state) {
4286
+ const results = [];
4287
+ if (!state.model && chunk.model) state.model = chunk.model;
4288
+ if (chunk.usage) {
4289
+ state.usage.promptTokens = chunk.usage.prompt_tokens;
4290
+ state.usage.completionTokens = chunk.usage.completion_tokens;
4291
+ state.usage.totalTokens = chunk.usage.total_tokens;
4292
+ }
4293
+ const choice = chunk.choices.at(0);
4294
+ if (!choice) return results;
4295
+ const delta = choice.delta;
4296
+ if (delta.tool_calls) for (const tc of delta.tool_calls) {
4297
+ const existing = state.toolCalls.get(tc.index);
4298
+ if (existing) {
4299
+ if (tc.function?.arguments) existing.args += tc.function.arguments;
4300
+ } else {
4301
+ const flushed = flushToolCalls(state, tc.index);
4302
+ if (flushed) results.push(flushed);
4303
+ state.toolCalls.set(tc.index, {
4304
+ name: tc.function?.name ?? "",
4305
+ args: tc.function?.arguments ?? ""
4306
+ });
4307
+ }
4308
+ }
4309
+ if (delta.content) results.push(buildGeminiChunk([{ text: delta.content }], choice.finish_reason, state));
4310
+ if (choice.finish_reason) {
4311
+ state.finishReason = choice.finish_reason;
4312
+ const flushed = flushToolCalls(state);
4313
+ if (flushed) results.push(flushed);
4314
+ if (!delta.content) results.push(buildGeminiChunk([], choice.finish_reason, state));
4315
+ }
4316
+ return results;
4317
+ }
4318
+ function flushToolCalls(state, belowIndex) {
4319
+ if (state.toolCalls.size === 0) return null;
4320
+ const parts = [];
4321
+ for (const [idx, tc] of state.toolCalls) {
4322
+ if (belowIndex !== void 0 && idx >= belowIndex) continue;
4323
+ const args = parseArgs(tc.args);
4324
+ parts.push({ functionCall: {
4325
+ name: tc.name,
4326
+ args
4327
+ } });
4328
+ state.toolCalls.delete(idx);
4329
+ }
4330
+ if (parts.length === 0) return null;
4331
+ return buildGeminiChunk(parts, null, state);
4332
+ }
4333
+ function buildGeminiChunk(parts, finishReason, state) {
4334
+ const candidate = {
4335
+ content: {
4336
+ role: "model",
4337
+ parts: parts.length > 0 ? parts : [{ text: "" }]
4338
+ },
4339
+ index: 0
4340
+ };
4341
+ if (finishReason) candidate.finishReason = mapFinishReason(finishReason);
4342
+ return {
4343
+ candidates: [candidate],
4344
+ usageMetadata: {
4345
+ promptTokenCount: state.usage.promptTokens,
4346
+ candidatesTokenCount: state.usage.completionTokens,
4347
+ totalTokenCount: state.usage.totalTokens
4348
+ },
4349
+ modelVersion: state.model
4350
+ };
4351
+ }
4352
+ function parseArgs(raw) {
4353
+ try {
4354
+ return JSON.parse(raw);
4355
+ } catch {
4356
+ return { raw };
4357
+ }
4358
+ }
4359
+ function mapFinishReason(reason) {
4360
+ switch (reason) {
4361
+ case "stop":
4362
+ case "tool_calls": return "STOP";
4363
+ case "length": return "MAX_TOKENS";
4364
+ case "content_filter": return "SAFETY";
4365
+ default: return "OTHER";
4366
+ }
4367
+ }
4368
+ function buildUsageMetadata(usage) {
4369
+ return {
4370
+ promptTokenCount: usage?.prompt_tokens ?? 0,
4371
+ candidatesTokenCount: usage?.completion_tokens ?? 0,
4372
+ totalTokenCount: usage?.total_tokens ?? 0
4373
+ };
4374
+ }
4375
+
4376
+ //#endregion
4377
+ //#region src/routes/gemini/handler.ts
4378
+ async function handleGeminiGenerate(c, model, isStream) {
4379
+ try {
4380
+ const geminiRequest = await c.req.json();
4381
+ consola.debug("Gemini request for model:", model, "stream:", isStream);
4382
+ const trackingId = c.get("trackingId");
4383
+ const startTime = Date.now();
4384
+ updateTrackerModel(trackingId, model);
4385
+ const { payload } = translateGeminiToOpenAI(geminiRequest, model);
4386
+ payload.stream = isStream;
4387
+ const selectedModel = state.models?.data.find((m) => m.id === model);
4388
+ if (isNullish(payload.max_tokens) && selectedModel) payload.max_tokens = selectedModel.capabilities?.limits?.max_output_tokens;
4389
+ const ctx = {
4390
+ historyId: recordRequest("gemini", {
4391
+ model,
4392
+ messages: payload.messages.map((m) => ({
4393
+ role: m.role,
4394
+ content: typeof m.content === "string" ? m.content : JSON.stringify(m.content),
4395
+ tool_calls: m.tool_calls,
4396
+ tool_call_id: m.tool_call_id
4397
+ })),
4398
+ stream: isStream,
4399
+ max_tokens: payload.max_tokens ?? void 0,
4400
+ temperature: payload.temperature ?? void 0
4401
+ }),
4402
+ trackingId,
4403
+ startTime
4404
+ };
4405
+ const { result: response, queueWaitMs } = await executeWithAdaptiveRateLimit(() => createChatCompletions(payload));
4406
+ ctx.queueWaitMs = queueWaitMs;
4407
+ if (isNonStreaming(response)) return handleNonStreamResponse(c, response, model, ctx, payload);
4408
+ consola.debug("Streaming Gemini response");
4409
+ updateTrackerStatus(trackingId, "streaming");
4410
+ return stream(c, async (s) => {
4411
+ c.header("Content-Type", "text/event-stream");
4412
+ c.header("Cache-Control", "no-cache");
4413
+ c.header("Connection", "keep-alive");
4414
+ const streamState = createGeminiStreamState();
4415
+ try {
4416
+ for await (const rawEvent of response) {
4417
+ if (rawEvent.data === "[DONE]") break;
4418
+ let chunk;
4419
+ try {
4420
+ chunk = JSON.parse(rawEvent.data);
4421
+ } catch (parseError) {
4422
+ consola.debug("Failed to parse stream chunk:", parseError);
4423
+ continue;
4424
+ }
4425
+ const geminiChunks = translateOpenAIChunkToGemini(chunk, streamState);
4426
+ for (const gc of geminiChunks) await s.write(`data: ${JSON.stringify(gc)}\n\n`);
4427
+ }
4428
+ recordResponse(ctx.historyId, {
4429
+ success: true,
4430
+ model: streamState.model || model,
4431
+ usage: {
4432
+ input_tokens: streamState.usage.promptTokens,
4433
+ output_tokens: streamState.usage.completionTokens
4434
+ },
4435
+ content: null
4436
+ }, Date.now() - ctx.startTime);
4437
+ completeTracking(ctx.trackingId, streamState.usage.promptTokens, streamState.usage.completionTokens, ctx.queueWaitMs, void 0, {
4438
+ model: streamState.model || model,
4439
+ stream: true,
4440
+ durationMs: Date.now() - ctx.startTime,
4441
+ stopReason: streamState.finishReason || void 0,
4442
+ toolCount: payload.tools?.length ?? 0
4443
+ });
4444
+ } catch (error) {
4445
+ recordStreamError({
4446
+ acc: { model: streamState.model || model },
4447
+ fallbackModel: model,
4448
+ ctx,
4449
+ error
4450
+ });
4451
+ failTracking(ctx.trackingId, error);
4452
+ }
4453
+ });
4454
+ } catch (error) {
4455
+ const trackingId = c.get("trackingId");
4456
+ if (trackingId) failTracking(trackingId, error);
4457
+ return forwardGeminiError(c, error);
4458
+ }
4459
+ }
4460
+ function handleNonStreamResponse(c, response, model, ctx, payload) {
4461
+ const geminiResponse = translateOpenAIResponseToGemini(response, model);
4462
+ const usage = response.usage;
4463
+ recordResponse(ctx.historyId, {
4464
+ success: true,
4465
+ model: response.model || model,
4466
+ usage: {
4467
+ input_tokens: usage?.prompt_tokens ?? 0,
4468
+ output_tokens: usage?.completion_tokens ?? 0
4469
+ },
4470
+ stop_reason: response.choices[0]?.finish_reason,
4471
+ content: response.choices[0] ? {
4472
+ role: "assistant",
4473
+ content: response.choices[0].message.content ?? ""
4474
+ } : null
4475
+ }, Date.now() - ctx.startTime);
4476
+ completeTracking(ctx.trackingId, usage?.prompt_tokens ?? 0, usage?.completion_tokens ?? 0, ctx.queueWaitMs, void 0, {
4477
+ model: response.model || model,
4478
+ stream: false,
4479
+ durationMs: Date.now() - ctx.startTime,
4480
+ stopReason: response.choices[0]?.finish_reason,
4481
+ toolCount: payload.tools?.length ?? 0
4482
+ });
4483
+ return c.json(geminiResponse);
4484
+ }
4485
+
4486
+ //#endregion
4487
+ //#region src/routes/gemini/route.ts
4488
+ const geminiRoutes = new Hono();
4489
+ geminiRoutes.post("/:modelAction", async (c) => {
4490
+ const modelAction = c.req.param("modelAction");
4491
+ const colonIndex = modelAction.lastIndexOf(":");
4492
+ if (colonIndex === -1) return geminiError(c, 400, "INVALID_ARGUMENT", "Missing action in URL");
4493
+ const model = modelAction.slice(0, Math.max(0, colonIndex));
4494
+ const action = modelAction.slice(Math.max(0, colonIndex + 1));
4495
+ switch (action) {
4496
+ case "generateContent": return handleGeminiGenerate(c, model, false);
4497
+ case "streamGenerateContent": return handleGeminiGenerate(c, model, true);
4498
+ case "countTokens": return handleGeminiCountTokens(c, model);
4499
+ default: return geminiError(c, 400, "INVALID_ARGUMENT", `Unknown action: ${action}`);
4500
+ }
4501
+ });
4502
+
4039
4503
  //#endregion
4040
4504
  //#region src/routes/history/api.ts
4041
4505
  function handleGetEntries(c) {
@@ -7946,6 +8410,7 @@ server.route("/v1/messages", messageRoutes);
7946
8410
  server.route("/api/event_logging", eventLoggingRoutes);
7947
8411
  server.route("/v1/responses", responsesRoutes);
7948
8412
  server.route("/responses", responsesRoutes);
8413
+ server.route("/v1beta/models", geminiRoutes);
7949
8414
  server.route("/history", historyRoutes);
7950
8415
 
7951
8416
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dianshuv/copilot-api",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "description": "Turn GitHub Copilot into OpenAI/Anthropic API compatible server. Usable with Claude Code!",
5
5
  "author": "dianshuv",
6
6
  "type": "module",