@dianshuv/copilot-api 0.6.0 → 0.6.2

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 +491 -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.2";
1040
1040
 
1041
1041
  //#endregion
1042
1042
  //#region src/lib/adaptive-rate-limiter.ts
@@ -4036,6 +4036,494 @@ 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/model-alias.ts
4488
+ /**
4489
+ * Maps Gemini model names that aren't available on GitHub Copilot
4490
+ * to equivalent models that are.
4491
+ *
4492
+ * The Gemini CLI's routing classifier requests gemini-2.5-flash-lite
4493
+ * and gemini-2.5-flash, which Copilot doesn't serve. We map them to
4494
+ * the closest available flash model.
4495
+ *
4496
+ * Aliases are only applied when the requested model is absent from
4497
+ * the Copilot model list, so if Copilot adds support for these models
4498
+ * natively, requests will go through unchanged.
4499
+ */
4500
+ const GEMINI_MODEL_ALIASES = {
4501
+ "gemini-2.5-flash-lite": "gemini-3-flash-preview",
4502
+ "gemini-2.5-flash": "gemini-3-flash-preview"
4503
+ };
4504
+ function resolveGeminiModelAlias(model) {
4505
+ if (!(model in GEMINI_MODEL_ALIASES)) return model;
4506
+ if (state.models?.data.some((m) => m.id === model)) return model;
4507
+ return GEMINI_MODEL_ALIASES[model];
4508
+ }
4509
+
4510
+ //#endregion
4511
+ //#region src/routes/gemini/route.ts
4512
+ const geminiRoutes = new Hono();
4513
+ geminiRoutes.post("/:modelAction", async (c) => {
4514
+ const modelAction = c.req.param("modelAction");
4515
+ const colonIndex = modelAction.lastIndexOf(":");
4516
+ if (colonIndex === -1) return geminiError(c, 400, "INVALID_ARGUMENT", "Missing action in URL");
4517
+ const model = resolveGeminiModelAlias(modelAction.slice(0, Math.max(0, colonIndex)));
4518
+ const action = modelAction.slice(Math.max(0, colonIndex + 1));
4519
+ switch (action) {
4520
+ case "generateContent": return handleGeminiGenerate(c, model, false);
4521
+ case "streamGenerateContent": return handleGeminiGenerate(c, model, true);
4522
+ case "countTokens": return handleGeminiCountTokens(c, model);
4523
+ default: return geminiError(c, 400, "INVALID_ARGUMENT", `Unknown action: ${action}`);
4524
+ }
4525
+ });
4526
+
4039
4527
  //#endregion
4040
4528
  //#region src/routes/history/api.ts
4041
4529
  function handleGetEntries(c) {
@@ -7946,6 +8434,7 @@ server.route("/v1/messages", messageRoutes);
7946
8434
  server.route("/api/event_logging", eventLoggingRoutes);
7947
8435
  server.route("/v1/responses", responsesRoutes);
7948
8436
  server.route("/responses", responsesRoutes);
8437
+ server.route("/v1beta/models", geminiRoutes);
7949
8438
  server.route("/history", historyRoutes);
7950
8439
 
7951
8440
  //#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.2",
4
4
  "description": "Turn GitHub Copilot into OpenAI/Anthropic API compatible server. Usable with Claude Code!",
5
5
  "author": "dianshuv",
6
6
  "type": "module",