Skip to content

Commit 8caabec

Browse files
ndbroadbentclaude
andcommitted
Add usage data to classifier and place-lookup APIs
Phase 5 of credit metering integration: - Add LlmUsage type (inputTokens, outputTokens) to track LLM API usage - Add PlaceLookupUsage type (placesSearchCalls, geocodingCalls) to track Places API usage - Update classifyMessages() to return ClassifyMessagesResult with activities + usage - Update classifyBatch() to return ClassifyBatchResult with activities + usage - Update lookupActivityPlace() to return LookupActivityResult with activity + usage - Update lookupActivityPlaces() to return LookupActivitiesResult with activities + usage - Add helper functions for adding usage data (addLlmUsage, addPlaceLookupUsage) - Add empty usage constants (EMPTY_LLM_USAGE, EMPTY_PLACE_LOOKUP_USAGE) - Export new types from shared module for lightweight imports - Update all tests and consumers to use new .activities property Breaking change: Consumer code must now access result.value.activities instead of result.value for classifier and place-lookup results. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent f13293b commit 8caabec

16 files changed

Lines changed: 307 additions & 124 deletions

src/caching/integration.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -205,9 +205,9 @@ describe('Cache Integration', () => {
205205

206206
// Results should have same content (timestamps may differ due to JSON serialization)
207207
if (result1.ok && result2.ok) {
208-
expect(result2.value).toHaveLength(result1.value.length)
209-
expect(result2.value[0]?.activity).toBe(result1.value[0]?.activity)
210-
expect(result2.value[0]?.city).toBe(result1.value[0]?.city)
208+
expect(result2.value.activities).toHaveLength(result1.value.activities.length)
209+
expect(result2.value.activities[0]?.activity).toBe(result1.value.activities[0]?.activity)
210+
expect(result2.value.activities[0]?.city).toBe(result1.value.activities[0]?.city)
211211
}
212212
})
213213

src/classifier/images-schema.integration.test.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ describe('Classifier Images Schema (IMAGES.md Examples)', () => {
7474
expect(result.ok).toBe(true)
7575
if (!result.ok) throw new Error(result.error.message)
7676

77-
const activity = result.value[0]
77+
const activity = result.value.activities[0]
7878
expect(activity).toBeDefined()
7979
if (!activity) throw new Error('No activity')
8080

@@ -115,7 +115,7 @@ describe('Classifier Images Schema (IMAGES.md Examples)', () => {
115115
expect(result.ok).toBe(true)
116116
if (!result.ok) throw new Error(result.error.message)
117117

118-
const activity = result.value[0]
118+
const activity = result.value.activities[0]
119119
expect(activity).toBeDefined()
120120
if (!activity) throw new Error('No activity')
121121

@@ -155,7 +155,7 @@ describe('Classifier Images Schema (IMAGES.md Examples)', () => {
155155
expect(result.ok).toBe(true)
156156
if (!result.ok) throw new Error(result.error.message)
157157

158-
const activity = result.value[0]
158+
const activity = result.value.activities[0]
159159
expect(activity).toBeDefined()
160160
if (!activity) throw new Error('No activity')
161161

@@ -197,7 +197,7 @@ describe('Classifier Images Schema (IMAGES.md Examples)', () => {
197197
expect(result.ok).toBe(true)
198198
if (!result.ok) throw new Error(result.error.message)
199199

200-
const activity = result.value[0]
200+
const activity = result.value.activities[0]
201201
expect(activity).toBeDefined()
202202
if (!activity) throw new Error('No activity')
203203

@@ -237,7 +237,7 @@ describe('Classifier Images Schema (IMAGES.md Examples)', () => {
237237
expect(result.ok).toBe(true)
238238
if (!result.ok) throw new Error(result.error.message)
239239

240-
const activity = result.value[0]
240+
const activity = result.value.activities[0]
241241
expect(activity).toBeDefined()
242242
if (!activity) throw new Error('No activity')
243243

@@ -277,7 +277,7 @@ describe('Classifier Images Schema (IMAGES.md Examples)', () => {
277277
expect(result.ok).toBe(true)
278278
if (!result.ok) throw new Error(result.error.message)
279279

280-
const activity = result.value[0]
280+
const activity = result.value.activities[0]
281281
expect(activity).toBeDefined()
282282
if (!activity) throw new Error('No activity')
283283

@@ -321,7 +321,7 @@ describe('Classifier Images Schema (IMAGES.md Examples)', () => {
321321
expect(result.ok).toBe(true)
322322
if (!result.ok) throw new Error(result.error.message)
323323

324-
const activity = result.value[0]
324+
const activity = result.value.activities[0]
325325
expect(activity).toBeDefined()
326326
if (!activity) throw new Error('No activity')
327327

src/classifier/index.test.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -265,9 +265,9 @@ describe('Classifier Module', () => {
265265

266266
expect(result.ok).toBe(true)
267267
if (result.ok) {
268-
expect(result.value).toHaveLength(1)
269-
expect(result.value[0]?.activity).toBe('Italian Restaurant')
270-
expect(result.value[0]?.category).toBe('food')
268+
expect(result.value.activities).toHaveLength(1)
269+
expect(result.value.activities[0]?.activity).toBe('Italian Restaurant')
270+
expect(result.value.activities[0]?.category).toBe('food')
271271
}
272272
})
273273

@@ -423,7 +423,7 @@ describe('Classifier Module', () => {
423423

424424
expect(result.ok).toBe(true)
425425
if (result.ok) {
426-
expect(result.value[0]?.category).toBe('other')
426+
expect(result.value.activities[0]?.category).toBe('other')
427427
}
428428
})
429429

@@ -519,7 +519,7 @@ describe('Classifier Module', () => {
519519

520520
expect(result.ok).toBe(true)
521521
if (result.ok) {
522-
expect(result.value[0]?.activity).toBe('Fallback Test')
522+
expect(result.value.activities[0]?.activity).toBe('Fallback Test')
523523
}
524524
expect(mockFetch).toHaveBeenCalledTimes(2)
525525
expect(mockFetch).toHaveBeenNthCalledWith(

src/classifier/index.ts

Lines changed: 42 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,13 @@ import type { ResponseCache } from '../caching/types'
1010
import { type EntityType, VALID_LINK_TYPES } from '../search/types'
1111
import {
1212
type ActivityCategory,
13+
addLlmUsage,
1314
type CandidateMessage,
1415
type ClassifiedActivity,
1516
type ClassifierConfig,
1617
calculateCombinedScore,
18+
EMPTY_LLM_USAGE,
19+
type LlmUsage,
1720
type Result
1821
} from '../types'
1922
import { DEFAULT_MODELS } from './models'
@@ -27,6 +30,18 @@ import { callProviderWithFallbacks } from './providers'
2730
import type { ParsedClassification } from './response-parser'
2831
import { countTokens, MAX_BATCH_TOKENS } from './tokenizer'
2932

33+
/** Result from classifyBatch including activities and usage data */
34+
export interface ClassifyBatchResult {
35+
readonly activities: ClassifiedActivity[]
36+
readonly usage: LlmUsage
37+
}
38+
39+
/** Result from classifyMessages including activities and total usage */
40+
export interface ClassifyMessagesResult {
41+
readonly activities: ClassifiedActivity[]
42+
readonly usage: LlmUsage
43+
}
44+
3045
export { createSmartBatches, groupCandidatesByProximity } from './batching'
3146
export type { ResolvedModel } from './models'
3247
export {
@@ -149,13 +164,14 @@ function toClassifiedActivity(
149164
* @param config Classifier configuration
150165
* @param cache Optional response cache
151166
* @param promptType Optional prompt type override (auto-detected if not specified)
167+
* @returns Result with activities AND usage data (usage is 0 for cache hits)
152168
*/
153169
export async function classifyBatch(
154170
candidates: readonly CandidateMessage[],
155171
config: ClassifierConfig,
156172
cache?: ResponseCache,
157173
promptType?: PromptType
158-
): Promise<Result<ClassifiedActivity[]>> {
174+
): Promise<Result<ClassifyBatchResult>> {
159175
const prompt = buildClassificationPrompt(
160176
candidates,
161177
{
@@ -191,7 +207,7 @@ export async function classifyBatch(
191207

192208
try {
193209
const expectedIds = candidates.map((c) => c.messageId)
194-
const parsed = parseClassificationResponse(responseResult.value, expectedIds)
210+
const parsed = parseClassificationResponse(responseResult.value.text, expectedIds)
195211

196212
// Map responses to candidates, filtering out invalid classifications
197213
const suggestions: ClassifiedActivity[] = []
@@ -206,7 +222,10 @@ export async function classifyBatch(
206222
}
207223
}
208224

209-
return { ok: true, value: suggestions }
225+
return {
226+
ok: true,
227+
value: { activities: suggestions, usage: responseResult.value.usage }
228+
}
210229
} catch (error) {
211230
const message = error instanceof Error ? error.message : String(error)
212231
return {
@@ -233,6 +252,12 @@ function createBatches(
233252
return batches
234253
}
235254

255+
/** Internal result from processBatches */
256+
interface ProcessBatchesResult {
257+
activities: ClassifiedActivity[]
258+
usage: LlmUsage
259+
}
260+
236261
/**
237262
* Process batches of a specific type.
238263
*/
@@ -244,8 +269,9 @@ async function processBatches(
244269
model: string,
245270
batchIndexOffset: number,
246271
totalBatches: number
247-
): Promise<Result<ClassifiedActivity[]>> {
272+
): Promise<Result<ProcessBatchesResult>> {
248273
const results: ClassifiedActivity[] = []
274+
let totalUsage: LlmUsage = EMPTY_LLM_USAGE
249275

250276
for (let i = 0; i < batches.length; i++) {
251277
const batch = batches[i]
@@ -281,14 +307,15 @@ async function processBatches(
281307
config.onBatchComplete?.({
282308
batchIndex: globalBatchIndex,
283309
totalBatches,
284-
activityCount: result.value.length,
310+
activityCount: result.value.activities.length,
285311
durationMs
286312
})
287313

288-
results.push(...result.value)
314+
results.push(...result.value.activities)
315+
totalUsage = addLlmUsage(totalUsage, result.value.usage)
289316
}
290317

291-
return { ok: true, value: results }
318+
return { ok: true, value: { activities: results, usage: totalUsage } }
292319
}
293320

294321
/**
@@ -300,13 +327,13 @@ async function processBatches(
300327
* @param candidates Candidate messages to classify
301328
* @param config Classifier configuration
302329
* @param cache Optional response cache to prevent duplicate API calls
303-
* @returns Classified suggestions or error
330+
* @returns Classified activities with aggregated usage data, or error
304331
*/
305332
export async function classifyMessages(
306333
candidates: readonly CandidateMessage[],
307334
config: ClassifierConfig,
308335
cache?: ResponseCache
309-
): Promise<Result<ClassifiedActivity[]>> {
336+
): Promise<Result<ClassifyMessagesResult>> {
310337
const batchSize = config.batchSize ?? DEFAULT_BATCH_SIZE
311338
const model = config.model ?? DEFAULT_MODELS[config.provider]
312339

@@ -319,6 +346,7 @@ export async function classifyMessages(
319346
const totalBatches = suggestionBatches.length + agreementBatches.length
320347

321348
const results: ClassifiedActivity[] = []
349+
let totalUsage: LlmUsage = EMPTY_LLM_USAGE
322350

323351
// Process suggestion batches first
324352
if (suggestionBatches.length > 0) {
@@ -334,7 +362,8 @@ export async function classifyMessages(
334362
if (!suggestionResult.ok) {
335363
return suggestionResult
336364
}
337-
results.push(...suggestionResult.value)
365+
results.push(...suggestionResult.value.activities)
366+
totalUsage = addLlmUsage(totalUsage, suggestionResult.value.usage)
338367
}
339368

340369
// Process agreement batches
@@ -351,10 +380,11 @@ export async function classifyMessages(
351380
if (!agreementResult.ok) {
352381
return agreementResult
353382
}
354-
results.push(...agreementResult.value)
383+
results.push(...agreementResult.value.activities)
384+
totalUsage = addLlmUsage(totalUsage, agreementResult.value.usage)
355385
}
356386

357-
return { ok: true, value: results }
387+
return { ok: true, value: { activities: results, usage: totalUsage } }
358388
}
359389

360390
/**

src/classifier/pronoun-resolution.integration.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ describe('Classifier Pronoun Resolution', () => {
7979

8080
// Verify the structure without checking exact timestamp (timezone-dependent)
8181
expect(result.value).toHaveLength(1)
82-
const activity = result.value[0]
82+
const activity = result.value.activities[0]
8383
expect(activity).toBeDefined()
8484
if (!activity) throw new Error('No activity found')
8585

0 commit comments

Comments
 (0)