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.
Files changed (54) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +241 -0
  3. package/dist/cli.d.ts +2 -0
  4. package/dist/cli.js +32 -0
  5. package/dist/lib/api-config.d.ts +15 -0
  6. package/dist/lib/api-config.js +30 -0
  7. package/dist/lib/database.d.ts +60 -0
  8. package/dist/lib/database.js +228 -0
  9. package/dist/lib/error.d.ts +11 -0
  10. package/dist/lib/error.js +34 -0
  11. package/dist/lib/state.d.ts +9 -0
  12. package/dist/lib/state.js +3 -0
  13. package/dist/lib/token-manager.d.ts +95 -0
  14. package/dist/lib/token-manager.js +241 -0
  15. package/dist/lib/utils.d.ts +8 -0
  16. package/dist/lib/utils.js +10 -0
  17. package/dist/main.d.ts +1 -0
  18. package/dist/main.js +97 -0
  19. package/dist/routes/anthropic/routes.d.ts +2 -0
  20. package/dist/routes/anthropic/routes.js +155 -0
  21. package/dist/routes/anthropic/stream-translation.d.ts +3 -0
  22. package/dist/routes/anthropic/stream-translation.js +136 -0
  23. package/dist/routes/anthropic/translation.d.ts +4 -0
  24. package/dist/routes/anthropic/translation.js +241 -0
  25. package/dist/routes/anthropic/types.d.ts +165 -0
  26. package/dist/routes/anthropic/types.js +2 -0
  27. package/dist/routes/anthropic/utils.d.ts +2 -0
  28. package/dist/routes/anthropic/utils.js +12 -0
  29. package/dist/routes/auth/routes.d.ts +2 -0
  30. package/dist/routes/auth/routes.js +158 -0
  31. package/dist/routes/gemini/routes.d.ts +2 -0
  32. package/dist/routes/gemini/routes.js +163 -0
  33. package/dist/routes/gemini/translation.d.ts +5 -0
  34. package/dist/routes/gemini/translation.js +215 -0
  35. package/dist/routes/gemini/types.d.ts +63 -0
  36. package/dist/routes/gemini/types.js +2 -0
  37. package/dist/routes/openai/routes.d.ts +2 -0
  38. package/dist/routes/openai/routes.js +215 -0
  39. package/dist/routes/utility/routes.d.ts +2 -0
  40. package/dist/routes/utility/routes.js +28 -0
  41. package/dist/services/copilot/create-chat-completions.d.ts +130 -0
  42. package/dist/services/copilot/create-chat-completions.js +32 -0
  43. package/dist/services/copilot/create-embeddings.d.ts +20 -0
  44. package/dist/services/copilot/create-embeddings.js +19 -0
  45. package/dist/services/copilot/get-models.d.ts +51 -0
  46. package/dist/services/copilot/get-models.js +45 -0
  47. package/dist/services/github/get-device-code.d.ts +11 -0
  48. package/dist/services/github/get-device-code.js +21 -0
  49. package/dist/services/github/get-user.d.ts +11 -0
  50. package/dist/services/github/get-user.js +17 -0
  51. package/dist/services/github/poll-access-token.d.ts +13 -0
  52. package/dist/services/github/poll-access-token.js +56 -0
  53. package/package.json +56 -0
  54. package/public/index.html +419 -0
@@ -0,0 +1,95 @@
1
+ import type { ModelsResponse } from "../services/copilot/get-models.js";
2
+ /**
3
+ * Token entry with runtime state
4
+ */
5
+ export interface TokenEntry {
6
+ id: number;
7
+ githubToken: string;
8
+ username: string | null;
9
+ copilotToken: string | null;
10
+ copilotTokenExpiresAt: Date | null;
11
+ accountType: string;
12
+ isActive: boolean;
13
+ models?: ModelsResponse;
14
+ lastUsed?: Date;
15
+ requestCount: number;
16
+ errorCount: number;
17
+ }
18
+ /**
19
+ * Token Manager - Manages multiple GitHub tokens with load balancing
20
+ */
21
+ declare class TokenManager {
22
+ private tokens;
23
+ private roundRobinIndex;
24
+ private nextMemoryId;
25
+ /**
26
+ * Get all token entries
27
+ */
28
+ getAllTokenEntries(): TokenEntry[];
29
+ /**
30
+ * Get active token entries only
31
+ */
32
+ getActiveTokenEntries(): TokenEntry[];
33
+ /**
34
+ * Get a random active token entry for load balancing
35
+ */
36
+ getRandomTokenEntry(): TokenEntry | null;
37
+ /**
38
+ * Get next token using round-robin for load balancing
39
+ */
40
+ getNextTokenEntry(): TokenEntry | null;
41
+ /**
42
+ * Get a specific token entry by ID
43
+ */
44
+ getTokenEntryById(id: number): TokenEntry | undefined;
45
+ /**
46
+ * Load tokens from database (only if database is connected)
47
+ * If database is not connected, try to load from local gh auth
48
+ */
49
+ loadFromDatabase(): Promise<void>;
50
+ /**
51
+ * Load token from local gh auth CLI
52
+ * This is used when database is not configured
53
+ */
54
+ loadFromLocalGhAuth(): Promise<void>;
55
+ /**
56
+ * Add a new token (from VSCode login)
57
+ */
58
+ addToken(githubToken: string, accountType?: string): Promise<TokenEntry>;
59
+ /**
60
+ * Remove a token
61
+ */
62
+ removeToken(id: number): Promise<boolean>;
63
+ /**
64
+ * Remove all tokens (for cleanup)
65
+ */
66
+ removeAllTokens(): Promise<void>;
67
+ /**
68
+ * Report an error for a token (for tracking)
69
+ */
70
+ reportError(id: number): void;
71
+ /**
72
+ * Get token count
73
+ */
74
+ getTokenCount(): {
75
+ total: number;
76
+ active: number;
77
+ };
78
+ /**
79
+ * Get statistics for all tokens
80
+ */
81
+ getStatistics(): TokenStatistics[];
82
+ }
83
+ export interface TokenStatistics {
84
+ id: number;
85
+ username: string | null;
86
+ accountType: string;
87
+ isActive: boolean;
88
+ hasValidCopilotToken: boolean;
89
+ copilotTokenExpiresAt: Date | null;
90
+ requestCount: number;
91
+ errorCount: number;
92
+ lastUsed?: Date;
93
+ }
94
+ export declare const tokenManager: TokenManager;
95
+ export {};
@@ -0,0 +1,241 @@
1
+ import consola from "consola";
2
+ import { exec } from "child_process";
3
+ import { promisify } from "util";
4
+ import { getAllTokens, saveToken, updateGithubToken, deactivateToken, isDatabaseConnected, } from "./database.js";
5
+ import { getGitHubUserForToken } from "../services/github/get-user.js";
6
+ const execAsync = promisify(exec);
7
+ /**
8
+ * Get the local GitHub auth token from gh CLI
9
+ * Returns null if gh is not installed or not authenticated
10
+ */
11
+ async function getLocalGhAuthToken() {
12
+ try {
13
+ const { stdout } = await execAsync("gh auth token");
14
+ const token = stdout.trim();
15
+ if (token && token.startsWith("gho_")) {
16
+ consola.debug(`Found local gh auth token: ${token.substring(0, 15)}...`);
17
+ return token;
18
+ }
19
+ return null;
20
+ }
21
+ catch (error) {
22
+ consola.debug("Failed to get local gh auth token:", error instanceof Error ? error.message : error);
23
+ return null;
24
+ }
25
+ }
26
+ /**
27
+ * Token Manager - Manages multiple GitHub tokens with load balancing
28
+ */
29
+ class TokenManager {
30
+ tokens = new Map();
31
+ roundRobinIndex = 0;
32
+ nextMemoryId = 1; // For generating IDs in memory-only mode
33
+ /**
34
+ * Get all token entries
35
+ */
36
+ getAllTokenEntries() {
37
+ return Array.from(this.tokens.values());
38
+ }
39
+ /**
40
+ * Get active token entries only
41
+ */
42
+ getActiveTokenEntries() {
43
+ return this.getAllTokenEntries().filter(t => t.isActive);
44
+ }
45
+ /**
46
+ * Get a random active token entry for load balancing
47
+ */
48
+ getRandomTokenEntry() {
49
+ const activeTokens = this.getActiveTokenEntries();
50
+ if (activeTokens.length === 0)
51
+ return null;
52
+ const randomIndex = Math.floor(Math.random() * activeTokens.length);
53
+ const token = activeTokens[randomIndex];
54
+ token.lastUsed = new Date();
55
+ token.requestCount++;
56
+ return token;
57
+ }
58
+ /**
59
+ * Get next token using round-robin for load balancing
60
+ */
61
+ getNextTokenEntry() {
62
+ const activeTokens = this.getActiveTokenEntries();
63
+ if (activeTokens.length === 0)
64
+ return null;
65
+ this.roundRobinIndex = this.roundRobinIndex % activeTokens.length;
66
+ const token = activeTokens[this.roundRobinIndex];
67
+ this.roundRobinIndex++;
68
+ token.lastUsed = new Date();
69
+ token.requestCount++;
70
+ return token;
71
+ }
72
+ /**
73
+ * Get a specific token entry by ID
74
+ */
75
+ getTokenEntryById(id) {
76
+ return this.tokens.get(id);
77
+ }
78
+ /**
79
+ * Load tokens from database (only if database is connected)
80
+ * If database is not connected, try to load from local gh auth
81
+ */
82
+ async loadFromDatabase() {
83
+ if (!isDatabaseConnected()) {
84
+ consola.info("Database not connected, trying to load local gh auth token...");
85
+ await this.loadFromLocalGhAuth();
86
+ return;
87
+ }
88
+ const dbTokens = await getAllTokens();
89
+ for (const record of dbTokens) {
90
+ const entry = {
91
+ id: record.Id,
92
+ githubToken: record.Token,
93
+ username: record.UserName,
94
+ copilotToken: null,
95
+ copilotTokenExpiresAt: null,
96
+ accountType: record.AccountType,
97
+ isActive: record.IsActive,
98
+ requestCount: 0,
99
+ errorCount: 0,
100
+ };
101
+ this.tokens.set(record.Id, entry);
102
+ // Update nextMemoryId to avoid conflicts
103
+ if (record.Id >= this.nextMemoryId) {
104
+ this.nextMemoryId = record.Id + 1;
105
+ }
106
+ }
107
+ consola.info(`Loaded ${this.tokens.size} tokens from database`);
108
+ }
109
+ /**
110
+ * Load token from local gh auth CLI
111
+ * This is used when database is not configured
112
+ */
113
+ async loadFromLocalGhAuth() {
114
+ const ghToken = await getLocalGhAuthToken();
115
+ if (!ghToken) {
116
+ consola.info("No local gh auth token found. Use /auth/login to add tokens or run 'gh auth login'.");
117
+ return;
118
+ }
119
+ try {
120
+ await this.addToken(ghToken, "individual");
121
+ consola.success("Loaded token from local gh auth");
122
+ }
123
+ catch (error) {
124
+ consola.warn("Failed to add local gh auth token:", error instanceof Error ? error.message : error);
125
+ }
126
+ }
127
+ /**
128
+ * Add a new token (from VSCode login)
129
+ */
130
+ async addToken(githubToken, accountType = "individual") {
131
+ // Get user info first to validate token
132
+ consola.debug(`addToken: Validating GitHub token: ${githubToken.substring(0, 15)}...`);
133
+ const user = await getGitHubUserForToken(githubToken);
134
+ const username = user.login;
135
+ consola.debug(`addToken: Token belongs to user: ${username}`);
136
+ // Check if token for this user already exists
137
+ const existingEntry = Array.from(this.tokens.values()).find(t => t.username === username);
138
+ if (existingEntry) {
139
+ // Update existing entry with new token
140
+ consola.debug(`addToken: Updating existing entry for ${username}, old token: ${existingEntry.githubToken.substring(0, 15)}...`);
141
+ existingEntry.githubToken = githubToken;
142
+ existingEntry.isActive = true;
143
+ existingEntry.errorCount = 0;
144
+ // Update in database if connected
145
+ if (isDatabaseConnected()) {
146
+ await updateGithubToken(existingEntry.id, githubToken);
147
+ }
148
+ consola.info(`Updated token for existing user: ${username}`);
149
+ return existingEntry;
150
+ }
151
+ // Generate ID - from database or memory
152
+ let id;
153
+ if (isDatabaseConnected()) {
154
+ id = await saveToken(githubToken, username, accountType);
155
+ consola.debug(`addToken: Saved new token with ID ${id}`);
156
+ }
157
+ else {
158
+ id = this.nextMemoryId++;
159
+ consola.debug(`addToken: Generated memory ID ${id}`);
160
+ }
161
+ // Create token entry
162
+ const entry = {
163
+ id,
164
+ githubToken,
165
+ username,
166
+ copilotToken: null,
167
+ copilotTokenExpiresAt: null,
168
+ accountType,
169
+ isActive: true,
170
+ requestCount: 0,
171
+ errorCount: 0,
172
+ };
173
+ this.tokens.set(id, entry);
174
+ consola.success(`Added token for user: ${username}`);
175
+ return entry;
176
+ }
177
+ /**
178
+ * Remove a token
179
+ */
180
+ async removeToken(id) {
181
+ const entry = this.tokens.get(id);
182
+ if (!entry)
183
+ return false;
184
+ // Deactivate in database if connected
185
+ if (isDatabaseConnected()) {
186
+ await deactivateToken(id);
187
+ }
188
+ this.tokens.delete(id);
189
+ consola.info(`Removed token for user: ${entry.username}`);
190
+ return true;
191
+ }
192
+ /**
193
+ * Remove all tokens (for cleanup)
194
+ */
195
+ async removeAllTokens() {
196
+ if (isDatabaseConnected()) {
197
+ const { deleteAllTokens } = await import("./database.js");
198
+ const count = await deleteAllTokens();
199
+ consola.info(`Removed ${count} tokens from database`);
200
+ }
201
+ this.tokens.clear();
202
+ consola.info("Cleared all tokens from memory");
203
+ }
204
+ /**
205
+ * Report an error for a token (for tracking)
206
+ */
207
+ reportError(id) {
208
+ const entry = this.tokens.get(id);
209
+ if (entry) {
210
+ entry.errorCount++;
211
+ }
212
+ }
213
+ /**
214
+ * Get token count
215
+ */
216
+ getTokenCount() {
217
+ const entries = Array.from(this.tokens.values());
218
+ return {
219
+ total: entries.length,
220
+ active: entries.filter(t => t.isActive && t.copilotToken).length,
221
+ };
222
+ }
223
+ /**
224
+ * Get statistics for all tokens
225
+ */
226
+ getStatistics() {
227
+ return Array.from(this.tokens.values()).map(entry => ({
228
+ id: entry.id,
229
+ username: entry.username,
230
+ accountType: entry.accountType,
231
+ isActive: entry.isActive,
232
+ hasValidCopilotToken: !!entry.copilotToken,
233
+ copilotTokenExpiresAt: entry.copilotTokenExpiresAt,
234
+ requestCount: entry.requestCount,
235
+ errorCount: entry.errorCount,
236
+ lastUsed: entry.lastUsed,
237
+ }));
238
+ }
239
+ }
240
+ // Singleton instance
241
+ export const tokenManager = new TokenManager();
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Utility functions for copilot-router
3
+ * Note: Token management has been moved to token-manager.ts
4
+ */
5
+ /**
6
+ * Sleep helper
7
+ */
8
+ export declare function sleep(ms: number): Promise<void>;
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Utility functions for copilot-router
3
+ * Note: Token management has been moved to token-manager.ts
4
+ */
5
+ /**
6
+ * Sleep helper
7
+ */
8
+ export function sleep(ms) {
9
+ return new Promise((resolve) => setTimeout(resolve, ms));
10
+ }
package/dist/main.d.ts ADDED
@@ -0,0 +1 @@
1
+ import "dotenv/config";
package/dist/main.js ADDED
@@ -0,0 +1,97 @@
1
+ import "dotenv/config";
2
+ import { serve } from "@hono/node-server";
3
+ import { serveStatic } from "@hono/node-server/serve-static";
4
+ import { OpenAPIHono } from "@hono/zod-openapi";
5
+ import { cors } from "hono/cors";
6
+ import { logger } from "hono/logger";
7
+ import consola from "consola";
8
+ import { join } from "path";
9
+ import { fileURLToPath } from "url";
10
+ import { dirname } from "path";
11
+ import { initializeDatabase } from "./lib/database.js";
12
+ import { tokenManager } from "./lib/token-manager.js";
13
+ import { registerOpenAIRoutes } from "./routes/openai/routes.js";
14
+ import { registerAnthropicRoutes } from "./routes/anthropic/routes.js";
15
+ import { registerGeminiRoutes } from "./routes/gemini/routes.js";
16
+ import { registerUtilityRoutes } from "./routes/utility/routes.js";
17
+ import { registerAuthRoutes } from "./routes/auth/routes.js";
18
+ const PORT = parseInt(process.env.PORT || "4242", 10);
19
+ const TOKEN_REFRESH_INTERVAL = 25 * 60 * 1000; // 25 minutes
20
+ // Get the package root directory (for static files)
21
+ const __filename = fileURLToPath(import.meta.url);
22
+ const __dirname = dirname(__filename);
23
+ // When running via npx, COPILOT_ROUTER_ROOT is set by CLI; otherwise use relative path
24
+ const PACKAGE_ROOT = process.env.COPILOT_ROUTER_ROOT || join(__dirname, "..");
25
+ const PUBLIC_DIR = join(PACKAGE_ROOT, "public");
26
+ async function main() {
27
+ consola.info("Starting Copilot Router...");
28
+ // Initialize database
29
+ consola.info("Connecting to SQL Server...");
30
+ await initializeDatabase();
31
+ // Load tokens from database
32
+ consola.info("Loading tokens from database...");
33
+ await tokenManager.loadFromDatabase();
34
+ // Refresh all Copilot tokens
35
+ const tokenCount = tokenManager.getTokenCount();
36
+ if (tokenCount.total > 0) {
37
+ consola.info(`Found ${tokenCount.total} tokens, refreshing Copilot tokens...`);
38
+ const updatedCount = tokenManager.getTokenCount();
39
+ consola.success(`${updatedCount.active} tokens active`);
40
+ }
41
+ else {
42
+ consola.warn("No tokens found. Use /auth/login to add tokens or run 'gh auth login'.");
43
+ }
44
+ // Create OpenAPI Hono app
45
+ const app = new OpenAPIHono();
46
+ // Middleware
47
+ app.use(logger());
48
+ app.use(cors());
49
+ // Register utility routes at root
50
+ registerUtilityRoutes(app);
51
+ // Register auth routes
52
+ registerAuthRoutes(app);
53
+ // OpenAI-compatible routes
54
+ const openaiRouter = new OpenAPIHono();
55
+ registerOpenAIRoutes(openaiRouter);
56
+ app.route("/", openaiRouter); // /chat/completions, /models, /embeddings
57
+ app.route("/v1", openaiRouter); // /v1/chat/completions, /v1/models, /v1/embeddings
58
+ // Anthropic-compatible routes
59
+ const anthropicRouter = new OpenAPIHono();
60
+ registerAnthropicRoutes(anthropicRouter);
61
+ app.route("/v1", anthropicRouter); // /v1/messages, /v1/messages/count_tokens
62
+ // Gemini-compatible routes
63
+ const geminiRouter = new OpenAPIHono();
64
+ registerGeminiRoutes(geminiRouter);
65
+ app.route("/v1beta", geminiRouter); // /v1beta/models/:model:generateContent
66
+ // Serve static files (login page)
67
+ app.use("/static/*", serveStatic({ root: PUBLIC_DIR, rewriteRequestPath: (path) => path.replace("/static", "") }));
68
+ app.get("/login", serveStatic({ path: join(PUBLIC_DIR, "index.html") }));
69
+ // OpenAPI documentation
70
+ app.doc("/openapi.json", {
71
+ openapi: "3.0.0",
72
+ info: {
73
+ title: "Copilot Router API",
74
+ version: "1.0.0",
75
+ description: "GitHub Copilot API with OpenAI, Anthropic, and Gemini compatibility",
76
+ },
77
+ });
78
+ // Start server
79
+ consola.info(`Starting server on port ${PORT}...`);
80
+ serve({
81
+ fetch: app.fetch,
82
+ port: PORT,
83
+ });
84
+ consola.success(`Copilot Router running at http://localhost:${PORT}`);
85
+ consola.info("Available endpoints:");
86
+ consola.info(" Web UI: GET /login");
87
+ consola.info(" Auth: POST /auth/login, POST /auth/complete, GET /auth/tokens");
88
+ consola.info(" OpenAI: POST /v1/chat/completions, GET /v1/models, POST /v1/embeddings");
89
+ consola.info(" Anthropic: POST /v1/messages, POST /v1/messages/count_tokens");
90
+ consola.info(" Gemini: POST /v1beta/models/:model:generateContent");
91
+ consola.info(" Utility: GET /, GET /token, GET /usage, GET /quota");
92
+ consola.info(" Docs: GET /openapi.json");
93
+ }
94
+ main().catch((error) => {
95
+ consola.error("Failed to start server:", error);
96
+ process.exit(1);
97
+ });
@@ -0,0 +1,2 @@
1
+ import { OpenAPIHono } from "@hono/zod-openapi";
2
+ export declare function registerAnthropicRoutes(app: OpenAPIHono): void;
@@ -0,0 +1,155 @@
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 { translateToAnthropic, translateToOpenAI } from "./translation.js";
7
+ import { translateChunkToAnthropicEvents } from "./stream-translation.js";
8
+ const AnthropicErrorResponseSchema = z.object({
9
+ type: z.literal("error"),
10
+ error: z.object({
11
+ type: z.string(),
12
+ message: z.string(),
13
+ }),
14
+ });
15
+ // Messages route
16
+ const messagesRoute = createRoute({
17
+ method: "post",
18
+ path: "/messages",
19
+ tags: ["Anthropic API"],
20
+ summary: "Create a message with Anthropic-compatible API",
21
+ description: "Create a message using the Anthropic-compatible API interface, powered by GitHub Copilot.",
22
+ request: {
23
+ body: {
24
+ content: {
25
+ "application/json": {
26
+ schema: z.object({}).passthrough(),
27
+ },
28
+ },
29
+ },
30
+ },
31
+ responses: {
32
+ 200: {
33
+ content: {
34
+ "application/json": {
35
+ schema: z.object({}).passthrough(),
36
+ },
37
+ "text/event-stream": {
38
+ schema: z.string(),
39
+ },
40
+ },
41
+ description: "Successfully created message",
42
+ },
43
+ 400: {
44
+ content: { "application/json": { schema: AnthropicErrorResponseSchema } },
45
+ description: "Bad request",
46
+ },
47
+ 500: {
48
+ content: { "application/json": { schema: AnthropicErrorResponseSchema } },
49
+ description: "Internal server error",
50
+ },
51
+ },
52
+ });
53
+ // Count tokens route
54
+ const countTokensRoute = createRoute({
55
+ method: "post",
56
+ path: "/messages/count_tokens",
57
+ tags: ["Anthropic API"],
58
+ summary: "Count input tokens for Anthropic-compatible messages",
59
+ description: "Count the input tokens for messages using the Anthropic-compatible API interface.",
60
+ request: {
61
+ body: {
62
+ content: {
63
+ "application/json": {
64
+ schema: z.object({}).passthrough(),
65
+ },
66
+ },
67
+ },
68
+ },
69
+ responses: {
70
+ 200: {
71
+ content: {
72
+ "application/json": {
73
+ schema: z.object({
74
+ input_tokens: z.number(),
75
+ }),
76
+ },
77
+ },
78
+ description: "Successfully counted input tokens",
79
+ },
80
+ 500: {
81
+ content: { "application/json": { schema: AnthropicErrorResponseSchema } },
82
+ description: "Internal server error",
83
+ },
84
+ },
85
+ });
86
+ const isNonStreaming = (response) => Object.hasOwn(response, "choices");
87
+ export function registerAnthropicRoutes(app) {
88
+ // POST /messages
89
+ app.openapi(messagesRoute, async (c) => {
90
+ try {
91
+ const anthropicPayload = await c.req.json();
92
+ consola.debug("Anthropic request payload:", JSON.stringify(anthropicPayload));
93
+ const openAIPayload = translateToOpenAI(anthropicPayload);
94
+ consola.debug("Translated OpenAI request payload:", JSON.stringify(openAIPayload));
95
+ const response = await createChatCompletions(openAIPayload);
96
+ if (isNonStreaming(response)) {
97
+ consola.debug("Non-streaming response from Copilot");
98
+ const anthropicResponse = translateToAnthropic(response);
99
+ return c.json(anthropicResponse);
100
+ }
101
+ consola.debug("Streaming response from Copilot");
102
+ return streamSSE(c, async (stream) => {
103
+ const streamState = {
104
+ messageStartSent: false,
105
+ contentBlockIndex: 0,
106
+ contentBlockOpen: false,
107
+ toolCalls: {},
108
+ };
109
+ for await (const rawEvent of response) {
110
+ if (rawEvent.data === "[DONE]")
111
+ break;
112
+ if (!rawEvent.data)
113
+ continue;
114
+ const chunk = JSON.parse(rawEvent.data);
115
+ const events = translateChunkToAnthropicEvents(chunk, streamState);
116
+ for (const event of events) {
117
+ await stream.writeSSE({
118
+ event: event.type,
119
+ data: JSON.stringify(event),
120
+ });
121
+ }
122
+ }
123
+ });
124
+ }
125
+ catch (error) {
126
+ return await forwardError(c, error);
127
+ }
128
+ });
129
+ // POST /messages/count_tokens
130
+ app.openapi(countTokensRoute, async (c) => {
131
+ try {
132
+ const anthropicPayload = await c.req.json();
133
+ const openAIPayload = translateToOpenAI(anthropicPayload);
134
+ // Simple estimation: count characters and divide by 4
135
+ let totalChars = 0;
136
+ for (const msg of openAIPayload.messages) {
137
+ if (typeof msg.content === "string") {
138
+ totalChars += msg.content.length;
139
+ }
140
+ else if (Array.isArray(msg.content)) {
141
+ for (const part of msg.content) {
142
+ if (part.type === "text") {
143
+ totalChars += part.text.length;
144
+ }
145
+ }
146
+ }
147
+ }
148
+ const estimatedTokens = Math.ceil(totalChars / 4);
149
+ return c.json({ input_tokens: estimatedTokens });
150
+ }
151
+ catch (error) {
152
+ return await forwardError(c, error);
153
+ }
154
+ });
155
+ }
@@ -0,0 +1,3 @@
1
+ import { type ChatCompletionChunk } from "../../services/copilot/create-chat-completions.js";
2
+ import { type AnthropicStreamEventData, type AnthropicStreamState } from "./types.js";
3
+ export declare function translateChunkToAnthropicEvents(chunk: ChatCompletionChunk, state: AnthropicStreamState): Array<AnthropicStreamEventData>;