-
Notifications
You must be signed in to change notification settings - Fork 567
Feature: Add Gemini and Z.ai (GLM) usage tracking #773
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: v0.15.0rc
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| import { Router, Request, Response } from 'express'; | ||
| import { GeminiProvider } from '../../providers/gemini-provider.js'; | ||
| import { getGeminiUsageService } from '../../services/gemini-usage-service.js'; | ||
| import { createLogger } from '@automaker/utils'; | ||
|
|
||
| const logger = createLogger('Gemini'); | ||
|
|
||
| export function createGeminiRoutes(): Router { | ||
| const router = Router(); | ||
|
|
||
| // Get current usage/quota data from Google Cloud API | ||
| router.get('/usage', async (_req: Request, res: Response) => { | ||
| try { | ||
| const usageService = getGeminiUsageService(); | ||
| const usageData = await usageService.fetchUsageData(); | ||
|
|
||
| res.json(usageData); | ||
| } catch (error) { | ||
| const message = error instanceof Error ? error.message : 'Unknown error'; | ||
| logger.error('Error fetching Gemini usage:', error); | ||
|
|
||
| // Return error in a format the UI expects | ||
| res.status(200).json({ | ||
| authenticated: false, | ||
| authMethod: 'none', | ||
| usedPercent: 0, | ||
| remainingPercent: 100, | ||
| lastUpdated: new Date().toISOString(), | ||
| error: `Failed to fetch Gemini usage: ${message}`, | ||
| }); | ||
| } | ||
| }); | ||
|
|
||
| // Check if Gemini is available | ||
| router.get('/status', async (_req: Request, res: Response) => { | ||
| try { | ||
| const provider = new GeminiProvider(); | ||
| const status = await provider.detectInstallation(); | ||
|
|
||
| const authMethod = | ||
| (status as any).authMethod || | ||
| (status.authenticated ? (status.hasApiKey ? 'api_key' : 'cli_login') : 'none'); | ||
|
|
||
| res.json({ | ||
| success: true, | ||
| installed: status.installed, | ||
| version: status.version || null, | ||
| path: status.path || null, | ||
| authenticated: status.authenticated || false, | ||
| authMethod, | ||
| hasCredentialsFile: (status as any).hasCredentialsFile || false, | ||
| }); | ||
| } catch (error) { | ||
| const message = error instanceof Error ? error.message : 'Unknown error'; | ||
| res.status(500).json({ success: false, error: message }); | ||
| } | ||
| }); | ||
|
Comment on lines
+35
to
+57
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Find GeminiProvider class and detectInstallation method
find . -type f -name "*.ts" -o -name "*.js" | xargs grep -l "class GeminiProvider" | head -5Repository: AutoMaker-Org/automaker Length of output: 113 🏁 Script executed: #!/bin/bash
# Search for detectInstallation method definition
rg "detectInstallation" --type ts --type tsx -B 5 -A 15Repository: AutoMaker-Org/automaker Length of output: 94 🏁 Script executed: #!/bin/bash
# Search for detectInstallation method with corrected flags
rg "detectInstallation" --type ts -B 5 -A 20Repository: AutoMaker-Org/automaker Length of output: 50379 🏁 Script executed: #!/bin/bash
# Read the GeminiProvider file
wc -l ./apps/server/src/providers/gemini-provider.tsRepository: AutoMaker-Org/automaker Length of output: 117 🏁 Script executed: #!/bin/bash
# Read the detectInstallation method in gemini-provider.ts
sed -n '110,145p' ./apps/server/src/providers/gemini-provider.tsRepository: AutoMaker-Org/automaker Length of output: 1064 🏁 Script executed: #!/bin/bash
# Also check InstallationStatus type definition
rg "interface InstallationStatus|type InstallationStatus" --type ts -A 15Repository: AutoMaker-Org/automaker Length of output: 963 🏁 Script executed: #!/bin/bash
# Read the full detectInstallation method in gemini-provider.ts to see exact return
rg "async detectInstallation" ./apps/server/src/providers/gemini-provider.ts -A 25Repository: AutoMaker-Org/automaker Length of output: 731 🏁 Script executed: #!/bin/bash
# Check if there's any place where status is modified before being used in the route
head -60 ./apps/server/src/routes/gemini/index.tsRepository: AutoMaker-Org/automaker Length of output: 2061 Remove unnecessary Lines 40-42 and 51 use 🤖 Prompt for AI Agents |
||
|
|
||
| return router; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,179 @@ | ||
| import { Router, Request, Response } from 'express'; | ||
| import { ZaiUsageService } from '../../services/zai-usage-service.js'; | ||
| import type { SettingsService } from '../../services/settings-service.js'; | ||
| import { createLogger } from '@automaker/utils'; | ||
|
|
||
| const logger = createLogger('Zai'); | ||
|
|
||
| export function createZaiRoutes( | ||
| usageService: ZaiUsageService, | ||
| settingsService: SettingsService | ||
| ): Router { | ||
| const router = Router(); | ||
|
|
||
| // Initialize z.ai API token from credentials on startup | ||
| (async () => { | ||
| try { | ||
| const credentials = await settingsService.getCredentials(); | ||
| if (credentials.apiKeys?.zai) { | ||
| usageService.setApiToken(credentials.apiKeys.zai); | ||
| logger.info('[init] Loaded z.ai API key from credentials'); | ||
| } | ||
| } catch (error) { | ||
| logger.error('[init] Failed to load z.ai API key from credentials:', error); | ||
| } | ||
| })(); | ||
|
|
||
| // Get current usage (fetches from z.ai API) | ||
| router.get('/usage', async (_req: Request, res: Response) => { | ||
| try { | ||
| // Check if z.ai API is configured | ||
| const isAvailable = usageService.isAvailable(); | ||
| if (!isAvailable) { | ||
| // Use a 200 + error payload so the UI doesn't interpret it as session auth error | ||
| res.status(200).json({ | ||
| error: 'z.ai API not configured', | ||
| message: 'Set Z_AI_API_KEY environment variable to enable z.ai usage tracking', | ||
| }); | ||
| return; | ||
| } | ||
|
|
||
| const usage = await usageService.fetchUsageData(); | ||
| res.json(usage); | ||
| } catch (error) { | ||
| const message = error instanceof Error ? error.message : 'Unknown error'; | ||
|
|
||
| if (message.includes('not configured') || message.includes('API token')) { | ||
| res.status(200).json({ | ||
| error: 'API token required', | ||
| message: 'Set Z_AI_API_KEY environment variable to enable z.ai usage tracking', | ||
| }); | ||
| } else if (message.includes('failed') || message.includes('request')) { | ||
| res.status(200).json({ | ||
| error: 'API request failed', | ||
| message: message, | ||
| }); | ||
| } else { | ||
| logger.error('Error fetching z.ai usage:', error); | ||
| res.status(500).json({ error: message }); | ||
| } | ||
| } | ||
| }); | ||
|
|
||
| // Configure API token (for settings page) | ||
| router.post('/configure', async (req: Request, res: Response) => { | ||
| try { | ||
| const { apiToken, apiHost } = req.body; | ||
|
|
||
| if (apiToken !== undefined) { | ||
| // Set in-memory token | ||
| usageService.setApiToken(apiToken || ''); | ||
|
|
||
| // Persist to credentials (deep merge happens in updateCredentials) | ||
| try { | ||
| await settingsService.updateCredentials({ | ||
| apiKeys: { zai: apiToken || '' }, | ||
| } as Parameters<typeof settingsService.updateCredentials>[0]); | ||
| logger.info('[configure] Saved z.ai API key to credentials'); | ||
| } catch (persistError) { | ||
| logger.error('[configure] Failed to persist z.ai API key:', persistError); | ||
| } | ||
| } | ||
|
|
||
| if (apiHost) { | ||
| usageService.setApiHost(apiHost); | ||
| } | ||
|
|
||
| res.json({ | ||
| success: true, | ||
| message: 'z.ai configuration updated', | ||
| isAvailable: usageService.isAvailable(), | ||
| }); | ||
| } catch (error) { | ||
| const message = error instanceof Error ? error.message : 'Unknown error'; | ||
| logger.error('Error configuring z.ai:', error); | ||
| res.status(500).json({ error: message }); | ||
| } | ||
| }); | ||
|
|
||
| // Verify API key without storing it (for testing in settings) | ||
| router.post('/verify', async (req: Request, res: Response) => { | ||
| try { | ||
| const { apiKey } = req.body; | ||
|
|
||
| if (!apiKey || typeof apiKey !== 'string' || apiKey.trim().length === 0) { | ||
| res.json({ | ||
| success: false, | ||
| authenticated: false, | ||
| error: 'Please provide an API key to test.', | ||
| }); | ||
| return; | ||
| } | ||
|
|
||
| // Test the key by making a request to z.ai API | ||
| const quotaUrl = | ||
| process.env.Z_AI_QUOTA_URL || | ||
| `${process.env.Z_AI_API_HOST ? `https://${process.env.Z_AI_API_HOST}` : 'https://api.z.ai'}/api/monitor/usage/quota/limit`; | ||
|
|
||
| logger.info(`[verify] Testing API key against: ${quotaUrl}`); | ||
|
|
||
| const response = await fetch(quotaUrl, { | ||
| method: 'GET', | ||
| headers: { | ||
| Authorization: `Bearer ${apiKey.trim()}`, | ||
| Accept: 'application/json', | ||
| }, | ||
| }); | ||
|
Comment on lines
+113
to
+126
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The logic for constructing the |
||
|
|
||
| if (response.ok) { | ||
| res.json({ | ||
| success: true, | ||
| authenticated: true, | ||
| message: 'Connection successful! z.ai API responded.', | ||
| }); | ||
| } else if (response.status === 401 || response.status === 403) { | ||
| res.json({ | ||
| success: false, | ||
| authenticated: false, | ||
| error: 'Invalid API key. Please check your key and try again.', | ||
| }); | ||
| } else { | ||
| res.json({ | ||
| success: false, | ||
| authenticated: false, | ||
| error: `API request failed: ${response.status} ${response.statusText}`, | ||
| }); | ||
| } | ||
| } catch (error) { | ||
|
Comment on lines
+120
to
+147
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Find package.json files in the project root and apps/server to check Node engine requirements
fd package.json -t f -E node_modules -E dist -E build | head -5Repository: AutoMaker-Org/automaker Length of output: 211 🏁 Script executed: #!/bin/bash
# Get the actual content of the file in question
cat -n apps/server/src/routes/zai/index.ts | head -170 | tail -60Repository: AutoMaker-Org/automaker Length of output: 2408 🏁 Script executed: #!/bin/bash
# Check for service directory and existing service files in apps/server
find apps/server/src/services -type f -name "*zai*" 2>/dev/null || echo "No zai service found"
ls -la apps/server/src/services/ 2>/dev/null | head -20Repository: AutoMaker-Org/automaker Length of output: 1493 🏁 Script executed: #!/bin/bash
# Check Node engine requirement in apps/server package.json
cat apps/server/package.json | grep -A 5 '"engines"'Repository: AutoMaker-Org/automaker Length of output: 179 🏁 Script executed: #!/bin/bash
# Get fuller context of the zai route file to check for event emission
cat -n apps/server/src/routes/zai/index.ts | head -160Repository: AutoMaker-Org/automaker Length of output: 6514 🏁 Script executed: #!/bin/bash
# Check zai-usage-service to understand service structure
head -60 apps/server/src/services/zai-usage-service.tsRepository: AutoMaker-Org/automaker Length of output: 1496 Add a timeout to the external z.ai verification request. Without a timeout, the fetch request can hang indefinitely during upstream stalls, blocking the server thread. Your Node 22 runtime fully supports Suggested fix const response = await fetch(quotaUrl, {
method: 'GET',
headers: {
Authorization: `Bearer ${apiKey.trim()}`,
Accept: 'application/json',
},
+ signal: AbortSignal.timeout(5000),
});🤖 Prompt for AI Agents |
||
| const message = error instanceof Error ? error.message : 'Unknown error'; | ||
| logger.error('Error verifying z.ai API key:', error); | ||
| res.json({ | ||
| success: false, | ||
| authenticated: false, | ||
| error: `Network error: ${message}`, | ||
| }); | ||
| } | ||
|
Comment on lines
+63
to
+155
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Move z.ai config/verify logic into a service and emit events. 🤖 Prompt for AI Agents |
||
| }); | ||
|
|
||
| // Check if z.ai is available | ||
| router.get('/status', async (_req: Request, res: Response) => { | ||
| try { | ||
| const isAvailable = usageService.isAvailable(); | ||
| const hasEnvApiKey = Boolean(process.env.Z_AI_API_KEY); | ||
| const hasApiKey = usageService.getApiToken() !== null; | ||
|
|
||
| res.json({ | ||
| success: true, | ||
| available: isAvailable, | ||
| hasApiKey, | ||
| hasEnvApiKey, | ||
| message: isAvailable ? 'z.ai API is configured' : 'z.ai API token not configured', | ||
| }); | ||
| } catch (error) { | ||
| const message = error instanceof Error ? error.message : 'Unknown error'; | ||
| res.status(500).json({ success: false, error: message }); | ||
| } | ||
| }); | ||
|
|
||
| return router; | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
catchblock for the/usageroute currently returns a200status withauthenticated: falsefor any unexpected error. This can be misleading, as an error (e.g., a file parsing error for credentials) doesn't necessarily mean the user is unauthenticated. For unexpected server-side errors, it's more appropriate to return a 500 status code. This provides a clearer signal to the client that something went wrong on the server, distinct from a normal "not authenticated" state which is handled gracefully within the service.