diff --git a/packages/core/src/core/loggingContentGenerator.test.ts b/packages/core/src/core/loggingContentGenerator.test.ts index 2b8249d539..7f3b1a9f33 100644 --- a/packages/core/src/core/loggingContentGenerator.test.ts +++ b/packages/core/src/core/loggingContentGenerator.test.ts @@ -315,6 +315,100 @@ describe('LoggingContentGenerator', () => { return true; }); }); + + it('should decode Uint8Array data in Gaxios errors', async () => { + const req = { contents: [], model: 'gemini-pro' }; + + const gaxiosError = Object.assign(new Error('Gaxios Error'), { + response: { data: new Uint8Array([72, 101, 108, 108, 111]) }, + }); + + vi.mocked(wrapped.generateContent).mockRejectedValue(gaxiosError); + + await expect( + loggingContentGenerator.generateContent( + req, + 'prompt-123', + LlmRole.MAIN, + ), + ).rejects.toSatisfy((error: unknown) => { + const gError = error as { response: { data: unknown } }; + expect(gError.response.data).toBe('Hello'); + return true; + }); + }); + + it('should decode multi-byte UTF-8 from comma-separated byte strings', async () => { + const req = { contents: [], model: 'gemini-pro' }; + + // "Héllo" in UTF-8 bytes: H=72, é=195,169, l=108, l=108, o=111 + const utf8Data = '72,195,169,108,108,111'; + const gaxiosError = Object.assign(new Error('Gaxios Error'), { + response: { data: utf8Data }, + }); + + vi.mocked(wrapped.generateContent).mockRejectedValue(gaxiosError); + + await expect( + loggingContentGenerator.generateContent( + req, + 'prompt-123', + LlmRole.MAIN, + ), + ).rejects.toSatisfy((error: unknown) => { + const gError = error as { response: { data: unknown } }; + expect(gError.response.data).toBe('Héllo'); + return true; + }); + }); + + it('should decode 3-byte UTF-8 from comma-separated byte strings', async () => { + const req = { contents: [], model: 'gemini-pro' }; + + // "こんにちは" in UTF-8 bytes (3 bytes per character) + const utf8Data = + '227,129,147,227,130,147,227,129,171,227,129,161,227,129,175'; + const gaxiosError = Object.assign(new Error('Gaxios Error'), { + response: { data: utf8Data }, + }); + + vi.mocked(wrapped.generateContent).mockRejectedValue(gaxiosError); + + await expect( + loggingContentGenerator.generateContent( + req, + 'prompt-123', + LlmRole.MAIN, + ), + ).rejects.toSatisfy((error: unknown) => { + const gError = error as { response: { data: unknown } }; + expect(gError.response.data).toBe('こんにちは'); + return true; + }); + }); + + it('should reject byte strings with values outside 0-255 range', async () => { + const req = { contents: [], model: 'gemini-pro' }; + + const outOfRange = '72,256,108'; + const gaxiosError = Object.assign(new Error('Gaxios Error'), { + response: { data: outOfRange }, + }); + + vi.mocked(wrapped.generateContent).mockRejectedValue(gaxiosError); + + await expect( + loggingContentGenerator.generateContent( + req, + 'prompt-123', + LlmRole.MAIN, + ), + ).rejects.toSatisfy((error: unknown) => { + const gError = error as { response: { data: unknown } }; + expect(gError.response.data).toBe(outOfRange); + return true; + }); + }); }); it('should NOT log error on AbortError (user cancellation)', async () => { diff --git a/packages/core/src/core/loggingContentGenerator.ts b/packages/core/src/core/loggingContentGenerator.ts index 027a7ae622..1c8579df9a 100644 --- a/packages/core/src/core/loggingContentGenerator.ts +++ b/packages/core/src/core/loggingContentGenerator.ts @@ -276,8 +276,10 @@ export class LoggingContentGenerator implements ContentGenerator { } private _fixGaxiosErrorData(error: unknown): void { - // Fix for raw ASCII buffer strings appearing in dev with the latest - // Gaxios updates. + // Fix for raw buffer data appearing in Gaxios errors. + // Gaxios may return the response body as a Uint8Array, a Buffer, or + // a string of comma-separated byte values (e.g. "72,101,108,108,111"). + // All three forms need to be decoded as UTF-8. if ( typeof error === 'object' && error !== null && @@ -288,11 +290,20 @@ export class LoggingContentGenerator implements ContentGenerator { ) { const response = error.response as { data: unknown }; const data = response.data; - if (typeof data === 'string' && data.includes(',')) { + + if (data instanceof Uint8Array) { + // Gaxios returned raw bytes directly + response.data = new TextDecoder().decode(data); + } else if (typeof data === 'string' && data.includes(',')) { + // Gaxios returned bytes as a comma-separated string try { - const charCodes = data.split(',').map(Number); - if (charCodes.every((code) => !isNaN(code))) { - response.data = String.fromCharCode(...charCodes); + const byteValues = data.split(',').map(Number); + if ( + byteValues.every((b) => Number.isInteger(b) && b >= 0 && b <= 255) + ) { + response.data = new TextDecoder().decode( + new Uint8Array(byteValues), + ); } } catch { // If parsing fails, just leave it alone