Skip to content

Commit 2586555

Browse files
Anthony Baileyclaude
andcommitted
feat: add opt-in API usage logging for write endpoint
- Implement comprehensive usage logging with rate limit tracking - Opt-in via write-usage.log file existence - Capture token usage, rate limits, duration, and errors - Clean wrapper pattern keeps server code uncluttered - Logs both successful API calls and rate limit errors - No performance impact when logging disabled This helps monitor API usage patterns and debug rate limit issues during conference demos and multi-user testing. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 404a33f commit 2586555

File tree

2 files changed

+250
-8
lines changed

2 files changed

+250
-8
lines changed

src/lib/usage-logger.ts

Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
import { writeFileSync, existsSync } from 'fs'
2+
import { join } from 'path'
3+
4+
const USAGE_LOG_FILE = 'write-usage.log'
5+
6+
interface UsageLogEntry {
7+
[requestId: string]: {
8+
timestamp: string // ISO format for lexical sorting
9+
stepName: string
10+
model: string
11+
orgId?: string
12+
usage: {
13+
input_tokens: number
14+
cache_creation_input_tokens: number
15+
cache_read_input_tokens: number
16+
output_tokens: number
17+
service_tier: string
18+
}
19+
rateLimits: {
20+
input_tokens_remaining: number
21+
output_tokens_remaining: number
22+
requests_remaining: number
23+
input_tokens_limit: number
24+
output_tokens_limit: number
25+
requests_limit: number
26+
}
27+
durationMs: number
28+
toolsUsed: boolean
29+
webSearchCount?: number
30+
}
31+
}
32+
33+
/**
34+
* Log API usage information if write-usage.log file exists
35+
* Uses JSON blob per line format for easy parsing
36+
*/
37+
export function logApiUsage(
38+
requestId: string,
39+
stepName: string,
40+
model: string,
41+
response: any, // Anthropic API response
42+
headers: Record<string, string>, // Response headers
43+
durationMs: number,
44+
toolsUsed: boolean = false,
45+
webSearchCount: number = 0
46+
): void {
47+
// Only log if the log file exists (opt-in)
48+
if (!existsSync(USAGE_LOG_FILE)) {
49+
return
50+
}
51+
52+
try {
53+
const logEntry: UsageLogEntry = {
54+
[requestId]: {
55+
timestamp: new Date().toISOString(),
56+
stepName,
57+
model,
58+
orgId: headers['anthropic-organization-id'],
59+
usage: response.usage || {
60+
input_tokens: 0,
61+
cache_creation_input_tokens: 0,
62+
cache_read_input_tokens: 0,
63+
output_tokens: 0,
64+
service_tier: 'unknown'
65+
},
66+
rateLimits: {
67+
input_tokens_remaining: parseInt(
68+
headers['anthropic-ratelimit-input-tokens-remaining'] || '0'
69+
),
70+
output_tokens_remaining: parseInt(
71+
headers['anthropic-ratelimit-output-tokens-remaining'] || '0'
72+
),
73+
requests_remaining: parseInt(headers['anthropic-ratelimit-requests-remaining'] || '0'),
74+
input_tokens_limit: parseInt(headers['anthropic-ratelimit-input-tokens-limit'] || '0'),
75+
output_tokens_limit: parseInt(headers['anthropic-ratelimit-output-tokens-limit'] || '0'),
76+
requests_limit: parseInt(headers['anthropic-ratelimit-requests-limit'] || '0')
77+
},
78+
durationMs,
79+
toolsUsed,
80+
...(webSearchCount > 0 && { webSearchCount })
81+
}
82+
}
83+
84+
// Append JSON blob as single line
85+
const logLine = JSON.stringify(logEntry) + '\n'
86+
writeFileSync(USAGE_LOG_FILE, logLine, { flag: 'a' })
87+
} catch (error) {
88+
// Silently fail to avoid breaking the API
89+
console.warn('Failed to log API usage:', error)
90+
}
91+
}
92+
93+
/**
94+
* Wrap an Anthropic API promise to optionally add logging with rate limit headers
95+
* If logging is disabled, returns the original response
96+
* If logging is enabled, uses .withResponse() to capture headers and logs usage
97+
*/
98+
export function optionallyLogUsage<T>(
99+
originalPromise: any, // The anthropic.messages.create() promise
100+
stepName: string,
101+
model: string,
102+
startTime: number,
103+
toolsUsed: boolean = false,
104+
webSearchCount: number = 0
105+
): Promise<T> {
106+
// If logging is disabled, return original promise unchanged
107+
if (!existsSync(USAGE_LOG_FILE)) {
108+
return originalPromise
109+
}
110+
111+
// If logging is enabled, use withResponse() to get headers
112+
return originalPromise
113+
.withResponse()
114+
.then((responseWithMeta: any) => {
115+
const response = responseWithMeta.data
116+
const headers = responseWithMeta.response.headers
117+
const durationMs = Date.now() - startTime
118+
119+
// Log in background (don't block)
120+
try {
121+
const logEntry: UsageLogEntry = {
122+
[response.id]: {
123+
timestamp: new Date().toISOString(),
124+
stepName,
125+
model,
126+
usage: response.usage || {
127+
input_tokens: 0,
128+
cache_creation_input_tokens: 0,
129+
cache_read_input_tokens: 0,
130+
output_tokens: 0,
131+
service_tier: 'unknown'
132+
},
133+
rateLimits: {
134+
input_tokens_remaining: parseInt(
135+
headers.get('anthropic-ratelimit-input-tokens-remaining') || '0'
136+
),
137+
output_tokens_remaining: parseInt(
138+
headers.get('anthropic-ratelimit-output-tokens-remaining') || '0'
139+
),
140+
requests_remaining: parseInt(
141+
headers.get('anthropic-ratelimit-requests-remaining') || '0'
142+
),
143+
input_tokens_limit: parseInt(
144+
headers.get('anthropic-ratelimit-input-tokens-limit') || '0'
145+
),
146+
output_tokens_limit: parseInt(
147+
headers.get('anthropic-ratelimit-output-tokens-limit') || '0'
148+
),
149+
requests_limit: parseInt(headers.get('anthropic-ratelimit-requests-limit') || '0'),
150+
// Check for potential web search rate limit headers (may not exist)
151+
web_search_remaining:
152+
parseInt(headers.get('anthropic-ratelimit-web-search-remaining') || '0') || null,
153+
web_search_limit:
154+
parseInt(headers.get('anthropic-ratelimit-web-search-limit') || '0') || null,
155+
// Also capture web search usage from response body if available
156+
web_search_requests_used: response.usage?.server_tool_use?.web_search_requests || null
157+
},
158+
durationMs,
159+
toolsUsed,
160+
...(webSearchCount > 0 && { webSearchCount })
161+
}
162+
}
163+
164+
// Append JSON blob as single line
165+
const logLine = JSON.stringify(logEntry) + '\n'
166+
writeFileSync(USAGE_LOG_FILE, logLine, { flag: 'a' })
167+
} catch (error) {
168+
// Silently fail to avoid breaking the API
169+
console.warn('Failed to log API usage:', error)
170+
}
171+
172+
// Return the original response (not the withResponse wrapper)
173+
return response
174+
})
175+
.catch((error: any) => {
176+
// Log errors with headers if possible
177+
const durationMs = Date.now() - startTime
178+
179+
try {
180+
const errorId = `error_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
181+
const logEntry = {
182+
[errorId]: {
183+
timestamp: new Date().toISOString(),
184+
stepName,
185+
model,
186+
error: error?.message || String(error),
187+
durationMs,
188+
toolsUsed,
189+
// Try to get rate limits from error response if available
190+
rateLimits: error?.headers
191+
? {
192+
input_tokens_remaining: parseInt(
193+
error.headers['anthropic-ratelimit-input-tokens-remaining'] || '0'
194+
),
195+
output_tokens_remaining: parseInt(
196+
error.headers['anthropic-ratelimit-output-tokens-remaining'] || '0'
197+
),
198+
requests_remaining: parseInt(
199+
error.headers['anthropic-ratelimit-requests-remaining'] || '0'
200+
),
201+
input_tokens_limit: parseInt(
202+
error.headers['anthropic-ratelimit-input-tokens-limit'] || '0'
203+
),
204+
output_tokens_limit: parseInt(
205+
error.headers['anthropic-ratelimit-output-tokens-limit'] || '0'
206+
),
207+
requests_limit: parseInt(
208+
error.headers['anthropic-ratelimit-requests-limit'] || '0'
209+
)
210+
}
211+
: null
212+
}
213+
}
214+
215+
// Append JSON blob as single line
216+
const logLine = JSON.stringify(logEntry) + '\n'
217+
writeFileSync(USAGE_LOG_FILE, logLine, { flag: 'a' })
218+
} catch (logError) {
219+
// Silently fail to avoid breaking the API
220+
console.warn('Failed to log API error:', logError)
221+
}
222+
223+
// Rethrow the original error
224+
throw error
225+
})
226+
}

src/routes/api/write/+server.ts

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { error, json } from '@sveltejs/kit'
22
import { env } from '$env/dynamic/private'
33
import Anthropic from '@anthropic-ai/sdk'
4+
import { optionallyLogUsage } from '$lib/usage-logger'
45

56
// Safely access the API key, will be undefined if not set
67
const ANTHROPIC_API_KEY_FOR_WRITE = env.ANTHROPIC_API_KEY_FOR_WRITE || undefined
@@ -52,7 +53,7 @@ const stepConfigs: Record<StepName, StepConfig> = {
5253
// Research-focused steps that benefit from web search
5354
findTarget: {
5455
toolsEnabled: true,
55-
maxToolCalls: 5,
56+
maxToolCalls: 3,
5657
description: 'Find possible targets (using web search)'
5758
},
5859
webSearch: {
@@ -287,12 +288,13 @@ Search for and provide:
287288
7. Contact information (professional email or official channels if publicly available)
288289
289290
Please cite all sources you use and only include information you can verify through your internet search. If you encounter conflicting information, note this and provide the most reliable source.
290-
291-
BE BRIEF! This is extremely important. Try to output only a few lines of text for each questions.
292-
BE FAST! You do not have a lot of time to answer this query before it times out!
293-
ANSWER QUICKLY!!!
294291
`
295292

293+
//BE BRIEF! This is extremely important. Try to output only a few lines of text for each questions.
294+
//BE FAST! You do not have a lot of time to answer this query before it times out!
295+
//ANSWER QUICKLY!!!
296+
//`
297+
296298
// Only initialize the client if we have an API key
297299
const anthropic = IS_API_AVAILABLE
298300
? new Anthropic({
@@ -358,6 +360,10 @@ async function callClaude(
358360
// Combine all the specified prompts
359361
const systemPrompt = promptNames.map((name) => System_Prompts[name]).join('')
360362

363+
// TEMP: Log the full request prompt for debugging
364+
console.debug(`${logPrefix} system prompt:\n---\n${systemPrompt}\n---`)
365+
console.debug(`${logPrefix} user content:\n---\n${userContent}\n---`)
366+
361367
// NEW: Determine if tools should be included in this call
362368
const shouldUseTools = toolsEnabled && ENABLE_WEB_SEARCH && IS_API_AVAILABLE
363369

@@ -372,14 +378,14 @@ async function callClaude(
372378
{
373379
type: 'web_search_20250305', // CHANGED: Use correct tool type from API docs
374380
name: 'web_search',
375-
max_uses: 1 // ADDED: Limit searches per request
381+
max_uses: 5 // ADDED: Limit searches per request
376382
}
377383
]
378384
: undefined
379385

380386
// ENHANCED: Create API request with conditional tool support
381387
const requestParams: any = {
382-
model: 'claude-3-7-sonnet-20250219',
388+
model: 'claude-sonnet-4-20250514',
383389
max_tokens: 4096,
384390
system: systemPrompt,
385391
messages: [{ role: 'user', content: userContent }]
@@ -406,7 +412,14 @@ async function callClaude(
406412
messages: currentMessages
407413
}
408414

409-
const response = await anthropic.messages.create(currentRequest)
415+
const response = await optionallyLogUsage(
416+
anthropic.messages.create(currentRequest),
417+
stepName,
418+
requestParams.model,
419+
startTime,
420+
shouldUseTools,
421+
toolCallCount
422+
)
410423

411424
// Log the request ID at debug level
412425
console.debug(`${logPrefix} requestId: ${response.id}`)
@@ -468,6 +481,9 @@ async function callClaude(
468481

469482
// Log the full response text at debug level
470483
console.debug(`${logPrefix} full response:\n---\n${finalText}\n---`)
484+
485+
// Logging is handled by optionallyLogUsage wrapper
486+
471487
return { text: finalText, durationSec: elapsed }
472488
} catch (error) {
473489
// ENHANCED: Better error handling for tool-related failures

0 commit comments

Comments
 (0)