diff --git a/containers/api-proxy/ai-credits-pricing.js b/containers/api-proxy/ai-credits-pricing.js index 752f16ee6..a4540073f 100644 --- a/containers/api-proxy/ai-credits-pricing.js +++ b/containers/api-proxy/ai-credits-pricing.js @@ -26,6 +26,9 @@ module.exports = Object.freeze({ 'gemini-3.1-flash': { input: 0.75, cachedInput: 0.075, cacheWrite: null, output: 4.50 }, 'gemini-3.1-pro': { input: 2.00, cachedInput: 0.20, cacheWrite: null, output: 12.00 }, 'gemini-3.5-flash': { input: 1.50, cachedInput: 0.15, cacheWrite: null, output: 9.00 }, - 'mai-code-1-flash': { input: 0.75, cachedInput: 0.075, cacheWrite: null, output: 4.50 }, - 'raptor-mini': { input: 0.25, cachedInput: 0.025, cacheWrite: null, output: 2.00 }, + 'mai-code-1-flash': { input: 0.75, cachedInput: 0.075, cacheWrite: null, output: 4.50 }, + 'raptor-mini': { input: 0.25, cachedInput: 0.025, cacheWrite: null, output: 2.00 }, + // Embedding models (output tokens are not produced; output cost is 0) + 'text-embedding-3-small': { input: 0.02, cachedInput: 0, cacheWrite: null, output: 0.00 }, + 'text-embedding-ada-002': { input: 0.10, cachedInput: 0, cacheWrite: null, output: 0.00 }, }); diff --git a/containers/api-proxy/guards/ai-credits-guard.test.js b/containers/api-proxy/guards/ai-credits-guard.test.js index 7290ebf50..6dc486ddb 100644 --- a/containers/api-proxy/guards/ai-credits-guard.test.js +++ b/containers/api-proxy/guards/ai-credits-guard.test.js @@ -121,6 +121,40 @@ describe('ai-credits-guard', () => { })); }); + it('resolves text-embedding-3-small with input-only pricing', () => { + const usage = applyAiCreditsUsage({ input_tokens: 1_000_000, output_tokens: 500 }, 'text-embedding-3-small'); + // input: 1M × $0.02/M / 10_000 denominator = 2.0 AIC + // output: 500 tokens × $0.00/M = 0 AIC (embedding models produce no output cost) + expect(usage).not.toBeNull(); + expect(usage.aiCreditsThisResponse).toBeCloseTo(2.0, 5); + expect(usage.outputCreditsThisResponse).toBe(0); + }); + + it('resolves text-embedding-3-small-inference via prefix match', () => { + const usage = applyAiCreditsUsage({ input_tokens: 1_000_000, output_tokens: 0 }, 'text-embedding-3-small-inference'); + // Same pricing as text-embedding-3-small via prefix match + expect(usage).not.toBeNull(); + expect(usage.aiCreditsThisResponse).toBeCloseTo(2.0, 5); + }); + + it('resolves text-embedding-ada-002 with input-only pricing', () => { + const usage = applyAiCreditsUsage({ input_tokens: 1_000_000, output_tokens: 1000 }, 'text-embedding-ada-002'); + // input: 1M × $0.10/M / 10_000 denominator = 10.0 AIC + // output: 1000 tokens × $0.00/M = 0 AIC (embedding models produce no output cost) + expect(usage).not.toBeNull(); + expect(usage.aiCreditsThisResponse).toBeCloseTo(10.0, 5); + expect(usage.outputCreditsThisResponse).toBe(0); + }); + + it('does not reject embedding models when maxAiCredits is active', () => { + process.env.AWF_MAX_AI_CREDITS = '10'; + resetAiCreditsGuardForTests(); + + expect(checkUnknownModelRejection('text-embedding-3-small')).toBeNull(); + expect(checkUnknownModelRejection('text-embedding-3-small-inference')).toBeNull(); + expect(checkUnknownModelRejection('text-embedding-ada-002')).toBeNull(); + }); + it('uses bundled models.dev pricing for known catalog models', () => { const usage = applyAiCreditsUsage({ input_tokens: 100,