copilot-router 1.0.0
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/LICENSE +21 -0
- package/README.md +241 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +32 -0
- package/dist/lib/api-config.d.ts +15 -0
- package/dist/lib/api-config.js +30 -0
- package/dist/lib/database.d.ts +60 -0
- package/dist/lib/database.js +228 -0
- package/dist/lib/error.d.ts +11 -0
- package/dist/lib/error.js +34 -0
- package/dist/lib/state.d.ts +9 -0
- package/dist/lib/state.js +3 -0
- package/dist/lib/token-manager.d.ts +95 -0
- package/dist/lib/token-manager.js +241 -0
- package/dist/lib/utils.d.ts +8 -0
- package/dist/lib/utils.js +10 -0
- package/dist/main.d.ts +1 -0
- package/dist/main.js +97 -0
- package/dist/routes/anthropic/routes.d.ts +2 -0
- package/dist/routes/anthropic/routes.js +155 -0
- package/dist/routes/anthropic/stream-translation.d.ts +3 -0
- package/dist/routes/anthropic/stream-translation.js +136 -0
- package/dist/routes/anthropic/translation.d.ts +4 -0
- package/dist/routes/anthropic/translation.js +241 -0
- package/dist/routes/anthropic/types.d.ts +165 -0
- package/dist/routes/anthropic/types.js +2 -0
- package/dist/routes/anthropic/utils.d.ts +2 -0
- package/dist/routes/anthropic/utils.js +12 -0
- package/dist/routes/auth/routes.d.ts +2 -0
- package/dist/routes/auth/routes.js +158 -0
- package/dist/routes/gemini/routes.d.ts +2 -0
- package/dist/routes/gemini/routes.js +163 -0
- package/dist/routes/gemini/translation.d.ts +5 -0
- package/dist/routes/gemini/translation.js +215 -0
- package/dist/routes/gemini/types.d.ts +63 -0
- package/dist/routes/gemini/types.js +2 -0
- package/dist/routes/openai/routes.d.ts +2 -0
- package/dist/routes/openai/routes.js +215 -0
- package/dist/routes/utility/routes.d.ts +2 -0
- package/dist/routes/utility/routes.js +28 -0
- package/dist/services/copilot/create-chat-completions.d.ts +130 -0
- package/dist/services/copilot/create-chat-completions.js +32 -0
- package/dist/services/copilot/create-embeddings.d.ts +20 -0
- package/dist/services/copilot/create-embeddings.js +19 -0
- package/dist/services/copilot/get-models.d.ts +51 -0
- package/dist/services/copilot/get-models.js +45 -0
- package/dist/services/github/get-device-code.d.ts +11 -0
- package/dist/services/github/get-device-code.js +21 -0
- package/dist/services/github/get-user.d.ts +11 -0
- package/dist/services/github/get-user.js +17 -0
- package/dist/services/github/poll-access-token.d.ts +13 -0
- package/dist/services/github/poll-access-token.js +56 -0
- package/package.json +56 -0
- package/public/index.html +419 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import consola from "consola";
|
|
2
|
+
import { tokenManager } from "../../lib/token-manager.js";
|
|
3
|
+
import { getDeviceCode } from "../../services/github/get-device-code.js";
|
|
4
|
+
import { checkAccessToken } from "../../services/github/poll-access-token.js";
|
|
5
|
+
// Track device codes being processed to prevent duplicate token additions
|
|
6
|
+
const processingDeviceCodes = new Set();
|
|
7
|
+
export function registerAuthRoutes(app) {
|
|
8
|
+
// POST /auth/login - Start device code flow
|
|
9
|
+
app.post("/auth/login", async (c) => {
|
|
10
|
+
try {
|
|
11
|
+
const deviceCode = await getDeviceCode();
|
|
12
|
+
consola.info(`Login initiated. Please visit ${deviceCode.verification_uri} and enter code: ${deviceCode.user_code}`);
|
|
13
|
+
return c.json({
|
|
14
|
+
user_code: deviceCode.user_code,
|
|
15
|
+
verification_uri: deviceCode.verification_uri,
|
|
16
|
+
device_code: deviceCode.device_code,
|
|
17
|
+
expires_in: deviceCode.expires_in,
|
|
18
|
+
interval: deviceCode.interval,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
consola.error("Failed to start login:", error);
|
|
23
|
+
return c.json({ error: { message: "Failed to start login", type: "auth_error" } }, 500);
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
// POST /auth/complete - Check device code flow status (single check, client polls)
|
|
27
|
+
app.post("/auth/complete", async (c) => {
|
|
28
|
+
try {
|
|
29
|
+
const body = await c.req.json();
|
|
30
|
+
const { device_code, account_type = "individual" } = body;
|
|
31
|
+
if (!device_code) {
|
|
32
|
+
return c.json({ error: { message: "device_code is required", type: "validation_error" } }, 400);
|
|
33
|
+
}
|
|
34
|
+
// Check access token once
|
|
35
|
+
const result = await checkAccessToken(device_code);
|
|
36
|
+
if (result.error === "authorization_pending") {
|
|
37
|
+
// User hasn't authorized yet, client should continue polling
|
|
38
|
+
return c.json({ status: "pending", message: "Waiting for user authorization" });
|
|
39
|
+
}
|
|
40
|
+
if (result.error === "slow_down") {
|
|
41
|
+
// Rate limited, client should slow down
|
|
42
|
+
return c.json({ status: "slow_down", message: "Please slow down polling" });
|
|
43
|
+
}
|
|
44
|
+
if (result.error === "expired_token") {
|
|
45
|
+
return c.json({ error: { message: "Device code expired. Please try again.", type: "expired" } }, 400);
|
|
46
|
+
}
|
|
47
|
+
if (result.error === "access_denied") {
|
|
48
|
+
return c.json({ error: { message: "Access denied by user.", type: "denied" } }, 400);
|
|
49
|
+
}
|
|
50
|
+
if (result.error) {
|
|
51
|
+
return c.json({ error: { message: result.error_description || result.error, type: "auth_error" } }, 400);
|
|
52
|
+
}
|
|
53
|
+
if (!result.access_token) {
|
|
54
|
+
return c.json({ status: "pending", message: "Waiting for user authorization" });
|
|
55
|
+
}
|
|
56
|
+
// Check if this device code is already being processed
|
|
57
|
+
if (processingDeviceCodes.has(device_code)) {
|
|
58
|
+
return c.json({ status: "processing", message: "Token is being added, please wait" });
|
|
59
|
+
}
|
|
60
|
+
// Mark as processing
|
|
61
|
+
processingDeviceCodes.add(device_code);
|
|
62
|
+
try {
|
|
63
|
+
// Success! Add token to manager
|
|
64
|
+
const entry = await tokenManager.addToken(result.access_token, account_type);
|
|
65
|
+
return c.json({
|
|
66
|
+
status: "success",
|
|
67
|
+
id: entry.id,
|
|
68
|
+
username: entry.username,
|
|
69
|
+
account_type: entry.accountType,
|
|
70
|
+
message: `Successfully logged in as ${entry.username}`,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
finally {
|
|
74
|
+
// Remove from processing set
|
|
75
|
+
processingDeviceCodes.delete(device_code);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
consola.error("Failed to complete login:", error);
|
|
80
|
+
return c.json({ error: { message: "Failed to complete login", type: "auth_error" } }, 500);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
// GET /auth/tokens - List all tokens
|
|
84
|
+
app.get("/auth/tokens", (c) => {
|
|
85
|
+
try {
|
|
86
|
+
const stats = tokenManager.getStatistics();
|
|
87
|
+
const counts = tokenManager.getTokenCount();
|
|
88
|
+
return c.json({
|
|
89
|
+
total: counts.total,
|
|
90
|
+
active: counts.active,
|
|
91
|
+
tokens: stats.map((s) => ({
|
|
92
|
+
id: s.id,
|
|
93
|
+
username: s.username,
|
|
94
|
+
account_type: s.accountType,
|
|
95
|
+
is_active: s.isActive,
|
|
96
|
+
has_copilot_token: s.hasValidCopilotToken,
|
|
97
|
+
copilot_token_expires_at: s.copilotTokenExpiresAt?.toISOString() ?? null,
|
|
98
|
+
request_count: s.requestCount,
|
|
99
|
+
error_count: s.errorCount,
|
|
100
|
+
last_used: s.lastUsed?.toISOString() ?? null,
|
|
101
|
+
})),
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
consola.error("Failed to list tokens:", error);
|
|
106
|
+
return c.json({ error: { message: "Failed to list tokens", type: "error" } }, 500);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
// DELETE /auth/tokens/all - Delete all tokens (for cleanup) - must be before :id route
|
|
110
|
+
app.delete("/auth/tokens/all", async (c) => {
|
|
111
|
+
try {
|
|
112
|
+
await tokenManager.removeAllTokens();
|
|
113
|
+
return c.json({ message: "All tokens deleted" });
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
consola.error("Failed to delete all tokens:", error);
|
|
117
|
+
return c.json({ error: { message: "Failed to delete all tokens", type: "error" } }, 500);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
// DELETE /auth/tokens/:id - Delete a token
|
|
121
|
+
app.delete("/auth/tokens/:id", async (c) => {
|
|
122
|
+
try {
|
|
123
|
+
const id = parseInt(c.req.param("id"), 10);
|
|
124
|
+
if (isNaN(id)) {
|
|
125
|
+
return c.json({ error: { message: "Invalid token ID", type: "validation_error" } }, 400);
|
|
126
|
+
}
|
|
127
|
+
const removed = await tokenManager.removeToken(id);
|
|
128
|
+
if (!removed) {
|
|
129
|
+
return c.json({ error: { message: "Token not found", type: "not_found" } }, 404);
|
|
130
|
+
}
|
|
131
|
+
return c.json({ message: "Token deleted successfully" });
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
consola.error("Failed to delete token:", error);
|
|
135
|
+
return c.json({ error: { message: "Failed to delete token", type: "error" } }, 500);
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
// POST /auth/tokens - Add token directly
|
|
139
|
+
app.post("/auth/tokens", async (c) => {
|
|
140
|
+
try {
|
|
141
|
+
const body = await c.req.json();
|
|
142
|
+
const { github_token, account_type = "individual" } = body;
|
|
143
|
+
if (!github_token) {
|
|
144
|
+
return c.json({ error: { message: "github_token is required", type: "validation_error" } }, 400);
|
|
145
|
+
}
|
|
146
|
+
const entry = await tokenManager.addToken(github_token, account_type);
|
|
147
|
+
return c.json({
|
|
148
|
+
id: entry.id,
|
|
149
|
+
username: entry.username,
|
|
150
|
+
message: `Token added for ${entry.username}`,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
consola.error("Failed to add token:", error);
|
|
155
|
+
return c.json({ error: { message: "Failed to add token", type: "error" } }, 500);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { createRoute, z } from "@hono/zod-openapi";
|
|
2
|
+
import { streamSSE } from "hono/streaming";
|
|
3
|
+
import consola from "consola";
|
|
4
|
+
import { forwardError } from "../../lib/error.js";
|
|
5
|
+
import { createChatCompletions, } from "../../services/copilot/create-chat-completions.js";
|
|
6
|
+
import { translateGeminiToOpenAI, translateOpenAIToGemini, translateChunkToGemini, } from "./translation.js";
|
|
7
|
+
const GeminiErrorResponseSchema = z.object({
|
|
8
|
+
error: z.object({
|
|
9
|
+
code: z.number(),
|
|
10
|
+
message: z.string(),
|
|
11
|
+
status: z.string(),
|
|
12
|
+
}),
|
|
13
|
+
});
|
|
14
|
+
// Generate content route (non-streaming)
|
|
15
|
+
const generateContentRoute = createRoute({
|
|
16
|
+
method: "post",
|
|
17
|
+
path: "/models/:modelWithMethod",
|
|
18
|
+
tags: ["Gemini API"],
|
|
19
|
+
summary: "Generate content with Gemini-compatible API",
|
|
20
|
+
description: "Generate content using the Gemini-compatible API interface, powered by GitHub Copilot.",
|
|
21
|
+
request: {
|
|
22
|
+
params: z.object({
|
|
23
|
+
modelWithMethod: z.string(),
|
|
24
|
+
}),
|
|
25
|
+
body: {
|
|
26
|
+
content: {
|
|
27
|
+
"application/json": {
|
|
28
|
+
schema: z.object({}).passthrough(),
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
responses: {
|
|
34
|
+
200: {
|
|
35
|
+
content: {
|
|
36
|
+
"application/json": {
|
|
37
|
+
schema: z.object({}).passthrough(),
|
|
38
|
+
},
|
|
39
|
+
"text/event-stream": {
|
|
40
|
+
schema: z.string(),
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
description: "Successfully generated content",
|
|
44
|
+
},
|
|
45
|
+
400: {
|
|
46
|
+
content: { "application/json": { schema: GeminiErrorResponseSchema } },
|
|
47
|
+
description: "Bad request",
|
|
48
|
+
},
|
|
49
|
+
500: {
|
|
50
|
+
content: { "application/json": { schema: GeminiErrorResponseSchema } },
|
|
51
|
+
description: "Internal server error",
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
// Count tokens route
|
|
56
|
+
const countTokensRoute = createRoute({
|
|
57
|
+
method: "post",
|
|
58
|
+
path: "/models/:modelWithMethod",
|
|
59
|
+
tags: ["Gemini API"],
|
|
60
|
+
summary: "Count tokens for Gemini-compatible request",
|
|
61
|
+
description: "Count the tokens for a request using the Gemini-compatible API interface.",
|
|
62
|
+
request: {
|
|
63
|
+
params: z.object({
|
|
64
|
+
modelWithMethod: z.string(),
|
|
65
|
+
}),
|
|
66
|
+
body: {
|
|
67
|
+
content: {
|
|
68
|
+
"application/json": {
|
|
69
|
+
schema: z.object({}).passthrough(),
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
responses: {
|
|
75
|
+
200: {
|
|
76
|
+
content: {
|
|
77
|
+
"application/json": {
|
|
78
|
+
schema: z.object({
|
|
79
|
+
totalTokens: z.number(),
|
|
80
|
+
}),
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
description: "Successfully counted tokens",
|
|
84
|
+
},
|
|
85
|
+
500: {
|
|
86
|
+
content: { "application/json": { schema: GeminiErrorResponseSchema } },
|
|
87
|
+
description: "Internal server error",
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
const isNonStreaming = (response) => Object.hasOwn(response, "choices");
|
|
92
|
+
function parseModelFromPath(modelWithMethod) {
|
|
93
|
+
// Format: gemini-2.5-pro:generateContent or gemini-2.5-pro:streamGenerateContent
|
|
94
|
+
const colonIndex = modelWithMethod.lastIndexOf(":");
|
|
95
|
+
if (colonIndex === -1) {
|
|
96
|
+
return { model: modelWithMethod, method: "generateContent" };
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
model: modelWithMethod.substring(0, colonIndex),
|
|
100
|
+
method: modelWithMethod.substring(colonIndex + 1),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
export function registerGeminiRoutes(app) {
|
|
104
|
+
// Handle all Gemini routes with pattern matching
|
|
105
|
+
app.post("/models/:modelWithMethod", async (c) => {
|
|
106
|
+
try {
|
|
107
|
+
const modelWithMethod = c.req.param("modelWithMethod");
|
|
108
|
+
const { model, method } = parseModelFromPath(modelWithMethod);
|
|
109
|
+
consola.debug(`Gemini request: model=${model}, method=${method}`);
|
|
110
|
+
const geminiPayload = await c.req.json();
|
|
111
|
+
if (method === "countTokens") {
|
|
112
|
+
// Simple token estimation
|
|
113
|
+
let totalChars = 0;
|
|
114
|
+
if (geminiPayload.contents) {
|
|
115
|
+
for (const content of geminiPayload.contents) {
|
|
116
|
+
for (const part of content.parts || []) {
|
|
117
|
+
if (part.text) {
|
|
118
|
+
totalChars += part.text.length;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (geminiPayload.systemInstruction) {
|
|
124
|
+
for (const part of geminiPayload.systemInstruction.parts || []) {
|
|
125
|
+
if (part.text) {
|
|
126
|
+
totalChars += part.text.length;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const estimatedTokens = Math.ceil(totalChars / 4);
|
|
131
|
+
return c.json({ totalTokens: estimatedTokens });
|
|
132
|
+
}
|
|
133
|
+
const isStreaming = method === "streamGenerateContent";
|
|
134
|
+
const openAIPayload = translateGeminiToOpenAI(geminiPayload, model);
|
|
135
|
+
openAIPayload.stream = isStreaming;
|
|
136
|
+
consola.debug("Translated OpenAI payload:", JSON.stringify(openAIPayload));
|
|
137
|
+
const response = await createChatCompletions(openAIPayload);
|
|
138
|
+
if (isNonStreaming(response)) {
|
|
139
|
+
const geminiResponse = translateOpenAIToGemini(response);
|
|
140
|
+
return c.json(geminiResponse);
|
|
141
|
+
}
|
|
142
|
+
// Streaming response
|
|
143
|
+
return streamSSE(c, async (stream) => {
|
|
144
|
+
for await (const rawEvent of response) {
|
|
145
|
+
if (rawEvent.data === "[DONE]")
|
|
146
|
+
break;
|
|
147
|
+
if (!rawEvent.data)
|
|
148
|
+
continue;
|
|
149
|
+
const chunk = JSON.parse(rawEvent.data);
|
|
150
|
+
const geminiChunk = translateChunkToGemini(chunk);
|
|
151
|
+
if (geminiChunk.candidates && geminiChunk.candidates.length > 0) {
|
|
152
|
+
await stream.writeSSE({
|
|
153
|
+
data: JSON.stringify(geminiChunk),
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
return await forwardError(c, error);
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { type ChatCompletionResponse, type ChatCompletionsPayload, type ChatCompletionChunk } from "../../services/copilot/create-chat-completions.js";
|
|
2
|
+
import { type GeminiGenerateContentRequest, type GeminiGenerateContentResponse } from "./types.js";
|
|
3
|
+
export declare function translateGeminiToOpenAI(request: GeminiGenerateContentRequest, model: string): ChatCompletionsPayload;
|
|
4
|
+
export declare function translateOpenAIToGemini(response: ChatCompletionResponse): GeminiGenerateContentResponse;
|
|
5
|
+
export declare function translateChunkToGemini(chunk: ChatCompletionChunk): GeminiGenerateContentResponse;
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
// Gemini -> OpenAI translation
|
|
2
|
+
export function translateGeminiToOpenAI(request, model) {
|
|
3
|
+
const messages = [];
|
|
4
|
+
// Handle system instruction
|
|
5
|
+
if (request.systemInstruction) {
|
|
6
|
+
const systemText = request.systemInstruction.parts
|
|
7
|
+
?.map((p) => p.text)
|
|
8
|
+
.filter(Boolean)
|
|
9
|
+
.join("\n");
|
|
10
|
+
if (systemText) {
|
|
11
|
+
messages.push({ role: "system", content: systemText });
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
// Handle contents
|
|
15
|
+
if (request.contents) {
|
|
16
|
+
for (const content of request.contents) {
|
|
17
|
+
const role = content.role === "model" ? "assistant" : "user";
|
|
18
|
+
const parts = content.parts || [];
|
|
19
|
+
// Check for function calls/responses
|
|
20
|
+
const functionCalls = parts.filter((p) => p.functionCall);
|
|
21
|
+
const functionResponses = parts.filter((p) => p.functionResponse);
|
|
22
|
+
const textParts = parts.filter((p) => p.text);
|
|
23
|
+
if (functionCalls.length > 0) {
|
|
24
|
+
// Assistant with tool calls
|
|
25
|
+
const textContent = textParts.map((p) => p.text).join("\n") || null;
|
|
26
|
+
messages.push({
|
|
27
|
+
role: "assistant",
|
|
28
|
+
content: textContent,
|
|
29
|
+
tool_calls: functionCalls.map((p, idx) => ({
|
|
30
|
+
id: p.functionCall.id || `call_${idx}`,
|
|
31
|
+
type: "function",
|
|
32
|
+
function: {
|
|
33
|
+
name: p.functionCall.name,
|
|
34
|
+
arguments: JSON.stringify(p.functionCall.args || {}),
|
|
35
|
+
},
|
|
36
|
+
})),
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
else if (functionResponses.length > 0) {
|
|
40
|
+
// Tool results
|
|
41
|
+
for (const p of functionResponses) {
|
|
42
|
+
messages.push({
|
|
43
|
+
role: "tool",
|
|
44
|
+
tool_call_id: p.functionResponse.id,
|
|
45
|
+
content: JSON.stringify(p.functionResponse.response || ""),
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
// Regular text message
|
|
51
|
+
const textContent = textParts.map((p) => p.text).join("\n") || "";
|
|
52
|
+
// Handle inline data (images)
|
|
53
|
+
const imageParts = parts.filter((p) => p.inlineData);
|
|
54
|
+
if (imageParts.length > 0) {
|
|
55
|
+
messages.push({
|
|
56
|
+
role: role,
|
|
57
|
+
content: [
|
|
58
|
+
...textParts.map((p) => ({ type: "text", text: p.text })),
|
|
59
|
+
...imageParts.map((p) => ({
|
|
60
|
+
type: "image_url",
|
|
61
|
+
image_url: {
|
|
62
|
+
url: `data:${p.inlineData.mimeType};base64,${p.inlineData.data}`,
|
|
63
|
+
},
|
|
64
|
+
})),
|
|
65
|
+
],
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
messages.push({
|
|
70
|
+
role: role,
|
|
71
|
+
content: textContent,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// Translate tools
|
|
78
|
+
let tools;
|
|
79
|
+
if (request.tools) {
|
|
80
|
+
tools = [];
|
|
81
|
+
for (const tool of request.tools) {
|
|
82
|
+
if (tool.functionDeclarations) {
|
|
83
|
+
for (const func of tool.functionDeclarations) {
|
|
84
|
+
tools.push({
|
|
85
|
+
type: "function",
|
|
86
|
+
function: {
|
|
87
|
+
name: func.name,
|
|
88
|
+
description: func.description,
|
|
89
|
+
parameters: func.parameters || {},
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// Translate tool config
|
|
97
|
+
let tool_choice;
|
|
98
|
+
if (request.toolConfig?.functionCallingConfig?.mode) {
|
|
99
|
+
const mode = request.toolConfig.functionCallingConfig.mode;
|
|
100
|
+
if (mode === "AUTO")
|
|
101
|
+
tool_choice = "auto";
|
|
102
|
+
else if (mode === "ANY")
|
|
103
|
+
tool_choice = "required";
|
|
104
|
+
else if (mode === "NONE")
|
|
105
|
+
tool_choice = "none";
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
model,
|
|
109
|
+
messages,
|
|
110
|
+
temperature: request.generationConfig?.temperature,
|
|
111
|
+
top_p: request.generationConfig?.topP,
|
|
112
|
+
max_tokens: request.generationConfig?.maxOutputTokens,
|
|
113
|
+
stop: request.generationConfig?.stopSequences,
|
|
114
|
+
tools: tools && tools.length > 0 ? tools : undefined,
|
|
115
|
+
tool_choice,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
// OpenAI -> Gemini translation
|
|
119
|
+
export function translateOpenAIToGemini(response) {
|
|
120
|
+
const candidates = [];
|
|
121
|
+
for (const choice of response.choices) {
|
|
122
|
+
const parts = [];
|
|
123
|
+
// Handle text content
|
|
124
|
+
if (choice.message.content) {
|
|
125
|
+
parts.push({ text: choice.message.content });
|
|
126
|
+
}
|
|
127
|
+
// Handle tool calls
|
|
128
|
+
if (choice.message.tool_calls) {
|
|
129
|
+
for (const toolCall of choice.message.tool_calls) {
|
|
130
|
+
parts.push({
|
|
131
|
+
functionCall: {
|
|
132
|
+
id: toolCall.id,
|
|
133
|
+
name: toolCall.function.name,
|
|
134
|
+
args: JSON.parse(toolCall.function.arguments),
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const content = {
|
|
140
|
+
parts,
|
|
141
|
+
role: "model",
|
|
142
|
+
};
|
|
143
|
+
const finishReasonMap = {
|
|
144
|
+
stop: "STOP",
|
|
145
|
+
length: "MAX_TOKENS",
|
|
146
|
+
tool_calls: "STOP",
|
|
147
|
+
content_filter: "SAFETY",
|
|
148
|
+
};
|
|
149
|
+
candidates.push({
|
|
150
|
+
content,
|
|
151
|
+
finishReason: finishReasonMap[choice.finish_reason] || "OTHER",
|
|
152
|
+
index: choice.index,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
return {
|
|
156
|
+
candidates,
|
|
157
|
+
usageMetadata: response.usage
|
|
158
|
+
? {
|
|
159
|
+
promptTokenCount: response.usage.prompt_tokens,
|
|
160
|
+
candidatesTokenCount: response.usage.completion_tokens,
|
|
161
|
+
totalTokenCount: response.usage.total_tokens,
|
|
162
|
+
}
|
|
163
|
+
: undefined,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
// Stream chunk translation
|
|
167
|
+
export function translateChunkToGemini(chunk) {
|
|
168
|
+
const candidates = [];
|
|
169
|
+
for (const choice of chunk.choices) {
|
|
170
|
+
const parts = [];
|
|
171
|
+
if (choice.delta.content) {
|
|
172
|
+
parts.push({ text: choice.delta.content });
|
|
173
|
+
}
|
|
174
|
+
if (choice.delta.tool_calls) {
|
|
175
|
+
for (const toolCall of choice.delta.tool_calls) {
|
|
176
|
+
if (toolCall.function?.name) {
|
|
177
|
+
parts.push({
|
|
178
|
+
functionCall: {
|
|
179
|
+
id: toolCall.id,
|
|
180
|
+
name: toolCall.function.name,
|
|
181
|
+
args: toolCall.function.arguments
|
|
182
|
+
? JSON.parse(toolCall.function.arguments)
|
|
183
|
+
: {},
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (parts.length > 0) {
|
|
190
|
+
const finishReasonMap = {
|
|
191
|
+
stop: "STOP",
|
|
192
|
+
length: "MAX_TOKENS",
|
|
193
|
+
tool_calls: "STOP",
|
|
194
|
+
content_filter: "SAFETY",
|
|
195
|
+
};
|
|
196
|
+
candidates.push({
|
|
197
|
+
content: { parts, role: "model" },
|
|
198
|
+
finishReason: choice.finish_reason
|
|
199
|
+
? finishReasonMap[choice.finish_reason] || "OTHER"
|
|
200
|
+
: undefined,
|
|
201
|
+
index: choice.index,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return {
|
|
206
|
+
candidates,
|
|
207
|
+
usageMetadata: chunk.usage
|
|
208
|
+
? {
|
|
209
|
+
promptTokenCount: chunk.usage.prompt_tokens,
|
|
210
|
+
candidatesTokenCount: chunk.usage.completion_tokens,
|
|
211
|
+
totalTokenCount: chunk.usage.total_tokens,
|
|
212
|
+
}
|
|
213
|
+
: undefined,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
export interface GeminiContent {
|
|
2
|
+
parts: GeminiPart[];
|
|
3
|
+
role?: "user" | "model";
|
|
4
|
+
}
|
|
5
|
+
export interface GeminiPart {
|
|
6
|
+
text?: string;
|
|
7
|
+
inlineData?: {
|
|
8
|
+
mimeType: string;
|
|
9
|
+
data: string;
|
|
10
|
+
};
|
|
11
|
+
functionCall?: {
|
|
12
|
+
id?: string;
|
|
13
|
+
name: string;
|
|
14
|
+
args?: Record<string, unknown>;
|
|
15
|
+
};
|
|
16
|
+
functionResponse?: {
|
|
17
|
+
id: string;
|
|
18
|
+
response?: unknown;
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export interface GeminiTool {
|
|
22
|
+
functionDeclarations?: GeminiFunctionDeclaration[];
|
|
23
|
+
}
|
|
24
|
+
export interface GeminiFunctionDeclaration {
|
|
25
|
+
name: string;
|
|
26
|
+
description?: string;
|
|
27
|
+
parameters?: Record<string, unknown>;
|
|
28
|
+
}
|
|
29
|
+
export interface GeminiToolConfig {
|
|
30
|
+
functionCallingConfig?: {
|
|
31
|
+
mode?: "AUTO" | "ANY" | "NONE";
|
|
32
|
+
allowedFunctionNames?: string[];
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
export interface GeminiGenerationConfig {
|
|
36
|
+
temperature?: number;
|
|
37
|
+
topP?: number;
|
|
38
|
+
topK?: number;
|
|
39
|
+
maxOutputTokens?: number;
|
|
40
|
+
stopSequences?: string[];
|
|
41
|
+
}
|
|
42
|
+
export interface GeminiGenerateContentRequest {
|
|
43
|
+
contents?: GeminiContent[];
|
|
44
|
+
tools?: GeminiTool[];
|
|
45
|
+
toolConfig?: GeminiToolConfig;
|
|
46
|
+
systemInstruction?: GeminiContent;
|
|
47
|
+
generationConfig?: GeminiGenerationConfig;
|
|
48
|
+
}
|
|
49
|
+
export interface GeminiCandidate {
|
|
50
|
+
content: GeminiContent;
|
|
51
|
+
finishReason?: "STOP" | "MAX_TOKENS" | "SAFETY" | "RECITATION" | "OTHER";
|
|
52
|
+
index?: number;
|
|
53
|
+
}
|
|
54
|
+
export interface GeminiUsageMetadata {
|
|
55
|
+
promptTokenCount?: number;
|
|
56
|
+
candidatesTokenCount?: number;
|
|
57
|
+
totalTokenCount?: number;
|
|
58
|
+
}
|
|
59
|
+
export interface GeminiGenerateContentResponse {
|
|
60
|
+
candidates?: GeminiCandidate[];
|
|
61
|
+
usageMetadata?: GeminiUsageMetadata;
|
|
62
|
+
modelVersion?: string;
|
|
63
|
+
}
|