|
3 | 3 | */ |
4 | 4 | import { describe, expect, it, vi } from 'vitest' |
5 | 5 |
|
6 | | -const { mockRecordCostCharged } = vi.hoisted(() => ({ mockRecordCostCharged: vi.fn() })) |
| 6 | +const { mockRecordCostCharged, mockRecordFailed } = vi.hoisted(() => ({ |
| 7 | + mockRecordCostCharged: vi.fn(), |
| 8 | + mockRecordFailed: vi.fn(), |
| 9 | +})) |
7 | 10 |
|
8 | 11 | vi.mock('@/providers/utils', () => ({ |
9 | 12 | calculateCost: vi.fn(() => ({ input: 1, output: 2, total: 3, pricing: {} })), |
10 | 13 | })) |
11 | 14 | vi.mock('@/lib/core/config/env-flags', () => ({ getCostMultiplier: () => 1 })) |
12 | 15 | vi.mock('@/lib/monitoring/metrics', () => ({ |
13 | | - hostedKeyMetrics: { recordCostCharged: mockRecordCostCharged }, |
| 16 | + hostedKeyMetrics: { recordCostCharged: mockRecordCostCharged, recordFailed: mockRecordFailed }, |
| 17 | +})) |
| 18 | +vi.mock('@/lib/api-key/hosted-cost', () => ({ |
| 19 | + classifyHostedKeyFailure: () => 'other', |
14 | 20 | })) |
15 | 21 |
|
16 | 22 | import type { NormalizedBlockOutput } from '@/executor/types' |
@@ -81,6 +87,41 @@ describe('createStreamingExecution', () => { |
81 | 87 | provider: 'openai', |
82 | 88 | tool: 'test-model', |
83 | 89 | }) |
| 90 | + expect(mockRecordFailed).not.toHaveBeenCalled() |
| 91 | + }) |
| 92 | + |
| 93 | + it('records a hosted-key failure (not cost) when the stream errors mid-drain', async () => { |
| 94 | + mockRecordCostCharged.mockClear() |
| 95 | + mockRecordFailed.mockClear() |
| 96 | + const boom = new Error('upstream 500') |
| 97 | + const sourceStream = new ReadableStream({ |
| 98 | + pull: (c) => c.error(boom), |
| 99 | + }) |
| 100 | + |
| 101 | + const result = createStreamingExecution({ |
| 102 | + model: 'test-model', |
| 103 | + providerStartTime, |
| 104 | + providerStartTimeISO, |
| 105 | + timing: { kind: 'simple', segmentName: 'test-model' }, |
| 106 | + initialTokens: { input: 0, output: 0, total: 0 }, |
| 107 | + initialCost: { input: 0, output: 0, total: 0 }, |
| 108 | + hostedKey: { provider: 'openai', envVar: 'OPENAI_API_KEY_1' }, |
| 109 | + cached: false, |
| 110 | + createStream: () => sourceStream, |
| 111 | + }) |
| 112 | + |
| 113 | + const reader = result.stream.getReader() |
| 114 | + await expect(reader.read()).rejects.toThrow('upstream 500') |
| 115 | + |
| 116 | + // Failure recorded once; no cost charged for a failed stream. |
| 117 | + expect(mockRecordFailed).toHaveBeenCalledTimes(1) |
| 118 | + expect(mockRecordFailed).toHaveBeenCalledWith({ |
| 119 | + provider: 'openai', |
| 120 | + tool: 'test-model', |
| 121 | + key: 'OPENAI_API_KEY_1', |
| 122 | + reason: 'other', |
| 123 | + }) |
| 124 | + expect(mockRecordCostCharged).not.toHaveBeenCalled() |
84 | 125 | }) |
85 | 126 |
|
86 | 127 | it('assembles the simple (no-tools) shape and finalizes timing on drain', () => { |
|
0 commit comments