From 61b1ed87eb3417747b84a8916150512199a6abad Mon Sep 17 00:00:00 2001 From: Hive Advise Date: Sat, 30 May 2026 15:43:56 -0400 Subject: [PATCH 1/2] Harden OpenAI-compatible AI adapter requests --- packages/ai/cerebras/src/index.test.ts | 29 +++++++++++++++++++++++--- packages/ai/cerebras/src/index.ts | 12 +++++++++-- packages/ai/deepseek/src/index.test.ts | 29 +++++++++++++++++++++++--- packages/ai/deepseek/src/index.ts | 12 +++++++++-- packages/ai/groq/src/index.test.ts | 29 +++++++++++++++++++++++--- packages/ai/groq/src/index.ts | 12 +++++++++-- packages/ai/mistral/src/index.test.ts | 29 +++++++++++++++++++++++--- packages/ai/mistral/src/index.ts | 12 +++++++++-- packages/ai/xai/src/index.test.ts | 29 +++++++++++++++++++++++--- packages/ai/xai/src/index.ts | 12 +++++++++-- 10 files changed, 180 insertions(+), 25 deletions(-) diff --git a/packages/ai/cerebras/src/index.test.ts b/packages/ai/cerebras/src/index.test.ts index 279d6d12..f96cdfb4 100644 --- a/packages/ai/cerebras/src/index.test.ts +++ b/packages/ai/cerebras/src/index.test.ts @@ -68,13 +68,36 @@ describe('Cerebras OpenAI-compatible generation', () => { }); }); - it('includes status and response body excerpt on errors', async () => { + it('normalizes configured base URLs with trailing slashes', async () => { + const fetchMock = vi.fn().mockResolvedValue({ + ok: true, + json: async () => ({ choices: [{ message: { content: 'ok' } }], model: 'llama-3.3-70b' }), + }); + vi.stubGlobal('fetch', fetchMock); + + await adapter.generate(ctx(), 'hello', {}, { baseUrl: 'https://proxy.example.com/' }); + + const [url] = fetchMock.mock.calls[0]!; + expect(url).toBe('https://proxy.example.com/v1/chat/completions'); + }); + + it('includes status and redacted response body excerpt on errors', async () => { + const apiKey = 'test-key'; vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: false, status: 500, - text: async () => 'server error'.repeat(30), + text: async () => `server error ${apiKey}`.repeat(30), })); - await expect(adapter.generate(ctx(), 'hello', {}, {})).rejects.toThrow(/Cerebras 500: server error/); + let error: unknown; + try { + await adapter.generate(ctx({ CEREBRAS_API_KEY: apiKey }), 'hello', {}, {}); + } catch (exc) { + error = exc; + } + expect(error).toBeInstanceOf(Error); + expect((error as Error).message).toContain('Cerebras 500: server error'); + expect((error as Error).message).toContain('[redacted]'); + expect((error as Error).message).not.toContain(apiKey); }); }); diff --git a/packages/ai/cerebras/src/index.ts b/packages/ai/cerebras/src/index.ts index 7576c2f7..a764b7af 100644 --- a/packages/ai/cerebras/src/index.ts +++ b/packages/ai/cerebras/src/index.ts @@ -6,6 +6,14 @@ interface Config { const DEFAULT_BASE = 'https://api.cerebras.ai'; +function chatCompletionsUrl(baseUrl?: string): string { + return `${(baseUrl ?? DEFAULT_BASE).replace(/\/+$/, '')}/v1/chat/completions`; +} + +function redact(value: string, apiKey: string): string { + return apiKey ? value.split(apiKey).join('[redacted]') : value; +} + export default defineAi({ id: 'ai-cerebras', label: 'Cerebras', @@ -23,7 +31,7 @@ export default defineAi({ if (opts.system) messages.push({ role: 'system', content: opts.system }); messages.push({ role: 'user', content: prompt }); - const res = await fetch(`${config.baseUrl ?? DEFAULT_BASE}/v1/chat/completions`, { + const res = await fetch(chatCompletionsUrl(config.baseUrl), { method: 'POST', headers: { authorization: `Bearer ${apiKey}`, @@ -37,7 +45,7 @@ export default defineAi({ ...opts.extra, }), }); - if (!res.ok) throw new Error(`Cerebras ${res.status}: ${(await res.text()).slice(0, 200)}`); + if (!res.ok) throw new Error(`Cerebras ${res.status}: ${redact((await res.text()).slice(0, 200), apiKey)}`); const data = (await res.json()) as { choices: Array<{ message?: { content?: string } }>; model: string; diff --git a/packages/ai/deepseek/src/index.test.ts b/packages/ai/deepseek/src/index.test.ts index 8fe67e33..1a5f2f49 100644 --- a/packages/ai/deepseek/src/index.test.ts +++ b/packages/ai/deepseek/src/index.test.ts @@ -68,13 +68,36 @@ describe('DeepSeek OpenAI-compatible generation', () => { }); }); - it('includes status and response body excerpt on errors', async () => { + it('normalizes configured base URLs with trailing slashes', async () => { + const fetchMock = vi.fn().mockResolvedValue({ + ok: true, + json: async () => ({ choices: [{ message: { content: 'ok' } }], model: 'deepseek-chat' }), + }); + vi.stubGlobal('fetch', fetchMock); + + await adapter.generate(ctx(), 'hello', {}, { baseUrl: 'https://proxy.example.com/' }); + + const [url] = fetchMock.mock.calls[0]!; + expect(url).toBe('https://proxy.example.com/v1/chat/completions'); + }); + + it('includes status and redacted response body excerpt on errors', async () => { + const apiKey = 'test-key'; vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: false, status: 401, - text: async () => 'invalid api key'.repeat(30), + text: async () => `invalid api key ${apiKey}`.repeat(30), })); - await expect(adapter.generate(ctx(), 'hello', {}, {})).rejects.toThrow(/DeepSeek 401: invalid api key/); + let error: unknown; + try { + await adapter.generate(ctx({ DEEPSEEK_API_KEY: apiKey }), 'hello', {}, {}); + } catch (exc) { + error = exc; + } + expect(error).toBeInstanceOf(Error); + expect((error as Error).message).toContain('DeepSeek 401: invalid api key'); + expect((error as Error).message).toContain('[redacted]'); + expect((error as Error).message).not.toContain(apiKey); }); }); diff --git a/packages/ai/deepseek/src/index.ts b/packages/ai/deepseek/src/index.ts index eba9438c..4b1e6206 100644 --- a/packages/ai/deepseek/src/index.ts +++ b/packages/ai/deepseek/src/index.ts @@ -6,6 +6,14 @@ interface Config { const DEFAULT_BASE = 'https://api.deepseek.com'; +function chatCompletionsUrl(baseUrl?: string): string { + return `${(baseUrl ?? DEFAULT_BASE).replace(/\/+$/, '')}/v1/chat/completions`; +} + +function redact(value: string, apiKey: string): string { + return apiKey ? value.split(apiKey).join('[redacted]') : value; +} + export default defineAi({ id: 'ai-deepseek', label: 'DeepSeek', @@ -23,7 +31,7 @@ export default defineAi({ if (opts.system) messages.push({ role: 'system', content: opts.system }); messages.push({ role: 'user', content: prompt }); - const res = await fetch(`${config.baseUrl ?? DEFAULT_BASE}/v1/chat/completions`, { + const res = await fetch(chatCompletionsUrl(config.baseUrl), { method: 'POST', headers: { authorization: `Bearer ${apiKey}`, @@ -37,7 +45,7 @@ export default defineAi({ ...opts.extra, }), }); - if (!res.ok) throw new Error(`DeepSeek ${res.status}: ${(await res.text()).slice(0, 200)}`); + if (!res.ok) throw new Error(`DeepSeek ${res.status}: ${redact((await res.text()).slice(0, 200), apiKey)}`); const data = (await res.json()) as { choices: Array<{ message?: { content?: string } }>; model: string; diff --git a/packages/ai/groq/src/index.test.ts b/packages/ai/groq/src/index.test.ts index b9d97011..132517ed 100644 --- a/packages/ai/groq/src/index.test.ts +++ b/packages/ai/groq/src/index.test.ts @@ -68,13 +68,36 @@ describe('Groq OpenAI-compatible generation', () => { }); }); - it('includes status and response body excerpt on errors', async () => { + it('normalizes configured base URLs with trailing slashes', async () => { + const fetchMock = vi.fn().mockResolvedValue({ + ok: true, + json: async () => ({ choices: [{ message: { content: 'ok' } }], model: 'llama-3.3-70b-versatile' }), + }); + vi.stubGlobal('fetch', fetchMock); + + await adapter.generate(ctx(), 'hello', {}, { baseUrl: 'https://proxy.example.com/' }); + + const [url] = fetchMock.mock.calls[0]!; + expect(url).toBe('https://proxy.example.com/v1/chat/completions'); + }); + + it('includes status and redacted response body excerpt on errors', async () => { + const apiKey = 'test-key'; vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: false, status: 429, - text: async () => 'rate limited'.repeat(30), + text: async () => `rate limited ${apiKey}`.repeat(30), })); - await expect(adapter.generate(ctx(), 'hello', {}, {})).rejects.toThrow(/Groq 429: rate limited/); + let error: unknown; + try { + await adapter.generate(ctx({ GROQ_API_KEY: apiKey }), 'hello', {}, {}); + } catch (exc) { + error = exc; + } + expect(error).toBeInstanceOf(Error); + expect((error as Error).message).toContain('Groq 429: rate limited'); + expect((error as Error).message).toContain('[redacted]'); + expect((error as Error).message).not.toContain(apiKey); }); }); diff --git a/packages/ai/groq/src/index.ts b/packages/ai/groq/src/index.ts index 2c2dd861..072bdc23 100644 --- a/packages/ai/groq/src/index.ts +++ b/packages/ai/groq/src/index.ts @@ -6,6 +6,14 @@ interface Config { const DEFAULT_BASE = 'https://api.groq.com/openai'; +function chatCompletionsUrl(baseUrl?: string): string { + return `${(baseUrl ?? DEFAULT_BASE).replace(/\/+$/, '')}/v1/chat/completions`; +} + +function redact(value: string, apiKey: string): string { + return apiKey ? value.split(apiKey).join('[redacted]') : value; +} + export default defineAi({ id: 'ai-groq', label: 'Groq', @@ -28,7 +36,7 @@ export default defineAi({ if (opts.system) messages.push({ role: 'system', content: opts.system }); messages.push({ role: 'user', content: prompt }); - const res = await fetch(`${config.baseUrl ?? DEFAULT_BASE}/v1/chat/completions`, { + const res = await fetch(chatCompletionsUrl(config.baseUrl), { method: 'POST', headers: { authorization: `Bearer ${apiKey}`, @@ -42,7 +50,7 @@ export default defineAi({ ...opts.extra, }), }); - if (!res.ok) throw new Error(`Groq ${res.status}: ${(await res.text()).slice(0, 200)}`); + if (!res.ok) throw new Error(`Groq ${res.status}: ${redact((await res.text()).slice(0, 200), apiKey)}`); const data = (await res.json()) as { choices: Array<{ message?: { content?: string } }>; model: string; diff --git a/packages/ai/mistral/src/index.test.ts b/packages/ai/mistral/src/index.test.ts index dd9646a5..565ae3ca 100644 --- a/packages/ai/mistral/src/index.test.ts +++ b/packages/ai/mistral/src/index.test.ts @@ -68,13 +68,36 @@ describe('Mistral OpenAI-compatible generation', () => { }); }); - it('includes status and response body excerpt on errors', async () => { + it('normalizes configured base URLs with trailing slashes', async () => { + const fetchMock = vi.fn().mockResolvedValue({ + ok: true, + json: async () => ({ choices: [{ message: { content: 'ok' } }], model: 'mistral-large-latest' }), + }); + vi.stubGlobal('fetch', fetchMock); + + await adapter.generate(ctx(), 'hello', {}, { baseUrl: 'https://proxy.example.com/' }); + + const [url] = fetchMock.mock.calls[0]!; + expect(url).toBe('https://proxy.example.com/v1/chat/completions'); + }); + + it('includes status and redacted response body excerpt on errors', async () => { + const apiKey = 'test-key'; vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: false, status: 400, - text: async () => 'bad request'.repeat(30), + text: async () => `bad request ${apiKey}`.repeat(30), })); - await expect(adapter.generate(ctx(), 'hello', {}, {})).rejects.toThrow(/Mistral 400: bad request/); + let error: unknown; + try { + await adapter.generate(ctx({ MISTRAL_API_KEY: apiKey }), 'hello', {}, {}); + } catch (exc) { + error = exc; + } + expect(error).toBeInstanceOf(Error); + expect((error as Error).message).toContain('Mistral 400: bad request'); + expect((error as Error).message).toContain('[redacted]'); + expect((error as Error).message).not.toContain(apiKey); }); }); diff --git a/packages/ai/mistral/src/index.ts b/packages/ai/mistral/src/index.ts index ec4fb718..b77c11c9 100644 --- a/packages/ai/mistral/src/index.ts +++ b/packages/ai/mistral/src/index.ts @@ -6,6 +6,14 @@ interface Config { const DEFAULT_BASE = 'https://api.mistral.ai'; +function chatCompletionsUrl(baseUrl?: string): string { + return `${(baseUrl ?? DEFAULT_BASE).replace(/\/+$/, '')}/v1/chat/completions`; +} + +function redact(value: string, apiKey: string): string { + return apiKey ? value.split(apiKey).join('[redacted]') : value; +} + export default defineAi({ id: 'ai-mistral', label: 'Mistral', @@ -23,7 +31,7 @@ export default defineAi({ if (opts.system) messages.push({ role: 'system', content: opts.system }); messages.push({ role: 'user', content: prompt }); - const res = await fetch(`${config.baseUrl ?? DEFAULT_BASE}/v1/chat/completions`, { + const res = await fetch(chatCompletionsUrl(config.baseUrl), { method: 'POST', headers: { authorization: `Bearer ${apiKey}`, @@ -37,7 +45,7 @@ export default defineAi({ ...opts.extra, }), }); - if (!res.ok) throw new Error(`Mistral ${res.status}: ${(await res.text()).slice(0, 200)}`); + if (!res.ok) throw new Error(`Mistral ${res.status}: ${redact((await res.text()).slice(0, 200), apiKey)}`); const data = (await res.json()) as { choices: Array<{ message?: { content?: string } }>; model: string; diff --git a/packages/ai/xai/src/index.test.ts b/packages/ai/xai/src/index.test.ts index ee64b8d1..d45775b9 100644 --- a/packages/ai/xai/src/index.test.ts +++ b/packages/ai/xai/src/index.test.ts @@ -68,13 +68,36 @@ describe('xAI OpenAI-compatible generation', () => { }); }); - it('includes status and response body excerpt on errors', async () => { + it('normalizes configured base URLs with trailing slashes', async () => { + const fetchMock = vi.fn().mockResolvedValue({ + ok: true, + json: async () => ({ choices: [{ message: { content: 'ok' } }], model: 'grok-3' }), + }); + vi.stubGlobal('fetch', fetchMock); + + await adapter.generate(ctx(), 'hello', {}, { baseUrl: 'https://proxy.example.com/' }); + + const [url] = fetchMock.mock.calls[0]!; + expect(url).toBe('https://proxy.example.com/v1/chat/completions'); + }); + + it('includes status and redacted response body excerpt on errors', async () => { + const apiKey = 'test-key'; vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: false, status: 403, - text: async () => 'forbidden'.repeat(30), + text: async () => `forbidden ${apiKey}`.repeat(30), })); - await expect(adapter.generate(ctx(), 'hello', {}, {})).rejects.toThrow(/xAI 403: forbidden/); + let error: unknown; + try { + await adapter.generate(ctx({ XAI_API_KEY: apiKey }), 'hello', {}, {}); + } catch (exc) { + error = exc; + } + expect(error).toBeInstanceOf(Error); + expect((error as Error).message).toContain('xAI 403: forbidden'); + expect((error as Error).message).toContain('[redacted]'); + expect((error as Error).message).not.toContain(apiKey); }); }); diff --git a/packages/ai/xai/src/index.ts b/packages/ai/xai/src/index.ts index fe124062..a6eec306 100644 --- a/packages/ai/xai/src/index.ts +++ b/packages/ai/xai/src/index.ts @@ -6,6 +6,14 @@ interface Config { const DEFAULT_BASE = 'https://api.x.ai'; +function chatCompletionsUrl(baseUrl?: string): string { + return `${(baseUrl ?? DEFAULT_BASE).replace(/\/+$/, '')}/v1/chat/completions`; +} + +function redact(value: string, apiKey: string): string { + return apiKey ? value.split(apiKey).join('[redacted]') : value; +} + export default defineAi({ id: 'ai-xai', label: 'xAI', @@ -23,7 +31,7 @@ export default defineAi({ if (opts.system) messages.push({ role: 'system', content: opts.system }); messages.push({ role: 'user', content: prompt }); - const res = await fetch(`${config.baseUrl ?? DEFAULT_BASE}/v1/chat/completions`, { + const res = await fetch(chatCompletionsUrl(config.baseUrl), { method: 'POST', headers: { authorization: `Bearer ${apiKey}`, @@ -37,7 +45,7 @@ export default defineAi({ ...opts.extra, }), }); - if (!res.ok) throw new Error(`xAI ${res.status}: ${(await res.text()).slice(0, 200)}`); + if (!res.ok) throw new Error(`xAI ${res.status}: ${redact((await res.text()).slice(0, 200), apiKey)}`); const data = (await res.json()) as { choices: Array<{ message?: { content?: string } }>; model: string; From 7b70765b85dc662089760fd9943bf76f6cd86487 Mon Sep 17 00:00:00 2001 From: Hive Advise Date: Sat, 30 May 2026 15:54:32 -0400 Subject: [PATCH 2/2] Redact adapter errors before truncating --- packages/ai/cerebras/src/index.test.ts | 8 +++++--- packages/ai/cerebras/src/index.ts | 2 +- packages/ai/deepseek/src/index.test.ts | 8 +++++--- packages/ai/deepseek/src/index.ts | 2 +- packages/ai/groq/src/index.test.ts | 8 +++++--- packages/ai/groq/src/index.ts | 2 +- packages/ai/mistral/src/index.test.ts | 8 +++++--- packages/ai/mistral/src/index.ts | 2 +- packages/ai/xai/src/index.test.ts | 8 +++++--- packages/ai/xai/src/index.ts | 2 +- 10 files changed, 30 insertions(+), 20 deletions(-) diff --git a/packages/ai/cerebras/src/index.test.ts b/packages/ai/cerebras/src/index.test.ts index f96cdfb4..2d79fa7d 100644 --- a/packages/ai/cerebras/src/index.test.ts +++ b/packages/ai/cerebras/src/index.test.ts @@ -82,11 +82,12 @@ describe('Cerebras OpenAI-compatible generation', () => { }); it('includes status and redacted response body excerpt on errors', async () => { - const apiKey = 'test-key'; + const apiKey = 'test-key-crossing-truncation-boundary'; + const prefix = 'x'.repeat(190); vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: false, status: 500, - text: async () => `server error ${apiKey}`.repeat(30), + text: async () => `${prefix}${apiKey} server error`, })); let error: unknown; @@ -96,8 +97,9 @@ describe('Cerebras OpenAI-compatible generation', () => { error = exc; } expect(error).toBeInstanceOf(Error); - expect((error as Error).message).toContain('Cerebras 500: server error'); + expect((error as Error).message).toContain('Cerebras 500:'); expect((error as Error).message).toContain('[redacted]'); expect((error as Error).message).not.toContain(apiKey); + expect((error as Error).message).not.toContain(apiKey.slice(0, 10)); }); }); diff --git a/packages/ai/cerebras/src/index.ts b/packages/ai/cerebras/src/index.ts index a764b7af..ddff214f 100644 --- a/packages/ai/cerebras/src/index.ts +++ b/packages/ai/cerebras/src/index.ts @@ -45,7 +45,7 @@ export default defineAi({ ...opts.extra, }), }); - if (!res.ok) throw new Error(`Cerebras ${res.status}: ${redact((await res.text()).slice(0, 200), apiKey)}`); + if (!res.ok) throw new Error(`Cerebras ${res.status}: ${redact(await res.text(), apiKey).slice(0, 200)}`); const data = (await res.json()) as { choices: Array<{ message?: { content?: string } }>; model: string; diff --git a/packages/ai/deepseek/src/index.test.ts b/packages/ai/deepseek/src/index.test.ts index 1a5f2f49..575c497e 100644 --- a/packages/ai/deepseek/src/index.test.ts +++ b/packages/ai/deepseek/src/index.test.ts @@ -82,11 +82,12 @@ describe('DeepSeek OpenAI-compatible generation', () => { }); it('includes status and redacted response body excerpt on errors', async () => { - const apiKey = 'test-key'; + const apiKey = 'test-key-crossing-truncation-boundary'; + const prefix = 'x'.repeat(190); vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: false, status: 401, - text: async () => `invalid api key ${apiKey}`.repeat(30), + text: async () => `${prefix}${apiKey} invalid api key`, })); let error: unknown; @@ -96,8 +97,9 @@ describe('DeepSeek OpenAI-compatible generation', () => { error = exc; } expect(error).toBeInstanceOf(Error); - expect((error as Error).message).toContain('DeepSeek 401: invalid api key'); + expect((error as Error).message).toContain('DeepSeek 401:'); expect((error as Error).message).toContain('[redacted]'); expect((error as Error).message).not.toContain(apiKey); + expect((error as Error).message).not.toContain(apiKey.slice(0, 10)); }); }); diff --git a/packages/ai/deepseek/src/index.ts b/packages/ai/deepseek/src/index.ts index 4b1e6206..f9244ae7 100644 --- a/packages/ai/deepseek/src/index.ts +++ b/packages/ai/deepseek/src/index.ts @@ -45,7 +45,7 @@ export default defineAi({ ...opts.extra, }), }); - if (!res.ok) throw new Error(`DeepSeek ${res.status}: ${redact((await res.text()).slice(0, 200), apiKey)}`); + if (!res.ok) throw new Error(`DeepSeek ${res.status}: ${redact(await res.text(), apiKey).slice(0, 200)}`); const data = (await res.json()) as { choices: Array<{ message?: { content?: string } }>; model: string; diff --git a/packages/ai/groq/src/index.test.ts b/packages/ai/groq/src/index.test.ts index 132517ed..66ba45f5 100644 --- a/packages/ai/groq/src/index.test.ts +++ b/packages/ai/groq/src/index.test.ts @@ -82,11 +82,12 @@ describe('Groq OpenAI-compatible generation', () => { }); it('includes status and redacted response body excerpt on errors', async () => { - const apiKey = 'test-key'; + const apiKey = 'test-key-crossing-truncation-boundary'; + const prefix = 'x'.repeat(190); vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: false, status: 429, - text: async () => `rate limited ${apiKey}`.repeat(30), + text: async () => `${prefix}${apiKey} rate limited`, })); let error: unknown; @@ -96,8 +97,9 @@ describe('Groq OpenAI-compatible generation', () => { error = exc; } expect(error).toBeInstanceOf(Error); - expect((error as Error).message).toContain('Groq 429: rate limited'); + expect((error as Error).message).toContain('Groq 429:'); expect((error as Error).message).toContain('[redacted]'); expect((error as Error).message).not.toContain(apiKey); + expect((error as Error).message).not.toContain(apiKey.slice(0, 10)); }); }); diff --git a/packages/ai/groq/src/index.ts b/packages/ai/groq/src/index.ts index 072bdc23..1f452b47 100644 --- a/packages/ai/groq/src/index.ts +++ b/packages/ai/groq/src/index.ts @@ -50,7 +50,7 @@ export default defineAi({ ...opts.extra, }), }); - if (!res.ok) throw new Error(`Groq ${res.status}: ${redact((await res.text()).slice(0, 200), apiKey)}`); + if (!res.ok) throw new Error(`Groq ${res.status}: ${redact(await res.text(), apiKey).slice(0, 200)}`); const data = (await res.json()) as { choices: Array<{ message?: { content?: string } }>; model: string; diff --git a/packages/ai/mistral/src/index.test.ts b/packages/ai/mistral/src/index.test.ts index 565ae3ca..1899498b 100644 --- a/packages/ai/mistral/src/index.test.ts +++ b/packages/ai/mistral/src/index.test.ts @@ -82,11 +82,12 @@ describe('Mistral OpenAI-compatible generation', () => { }); it('includes status and redacted response body excerpt on errors', async () => { - const apiKey = 'test-key'; + const apiKey = 'test-key-crossing-truncation-boundary'; + const prefix = 'x'.repeat(190); vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: false, status: 400, - text: async () => `bad request ${apiKey}`.repeat(30), + text: async () => `${prefix}${apiKey} bad request`, })); let error: unknown; @@ -96,8 +97,9 @@ describe('Mistral OpenAI-compatible generation', () => { error = exc; } expect(error).toBeInstanceOf(Error); - expect((error as Error).message).toContain('Mistral 400: bad request'); + expect((error as Error).message).toContain('Mistral 400:'); expect((error as Error).message).toContain('[redacted]'); expect((error as Error).message).not.toContain(apiKey); + expect((error as Error).message).not.toContain(apiKey.slice(0, 10)); }); }); diff --git a/packages/ai/mistral/src/index.ts b/packages/ai/mistral/src/index.ts index b77c11c9..2b4ab799 100644 --- a/packages/ai/mistral/src/index.ts +++ b/packages/ai/mistral/src/index.ts @@ -45,7 +45,7 @@ export default defineAi({ ...opts.extra, }), }); - if (!res.ok) throw new Error(`Mistral ${res.status}: ${redact((await res.text()).slice(0, 200), apiKey)}`); + if (!res.ok) throw new Error(`Mistral ${res.status}: ${redact(await res.text(), apiKey).slice(0, 200)}`); const data = (await res.json()) as { choices: Array<{ message?: { content?: string } }>; model: string; diff --git a/packages/ai/xai/src/index.test.ts b/packages/ai/xai/src/index.test.ts index d45775b9..6c7c0784 100644 --- a/packages/ai/xai/src/index.test.ts +++ b/packages/ai/xai/src/index.test.ts @@ -82,11 +82,12 @@ describe('xAI OpenAI-compatible generation', () => { }); it('includes status and redacted response body excerpt on errors', async () => { - const apiKey = 'test-key'; + const apiKey = 'test-key-crossing-truncation-boundary'; + const prefix = 'x'.repeat(190); vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: false, status: 403, - text: async () => `forbidden ${apiKey}`.repeat(30), + text: async () => `${prefix}${apiKey} forbidden`, })); let error: unknown; @@ -96,8 +97,9 @@ describe('xAI OpenAI-compatible generation', () => { error = exc; } expect(error).toBeInstanceOf(Error); - expect((error as Error).message).toContain('xAI 403: forbidden'); + expect((error as Error).message).toContain('xAI 403:'); expect((error as Error).message).toContain('[redacted]'); expect((error as Error).message).not.toContain(apiKey); + expect((error as Error).message).not.toContain(apiKey.slice(0, 10)); }); }); diff --git a/packages/ai/xai/src/index.ts b/packages/ai/xai/src/index.ts index a6eec306..ec10c272 100644 --- a/packages/ai/xai/src/index.ts +++ b/packages/ai/xai/src/index.ts @@ -45,7 +45,7 @@ export default defineAi({ ...opts.extra, }), }); - if (!res.ok) throw new Error(`xAI ${res.status}: ${redact((await res.text()).slice(0, 200), apiKey)}`); + if (!res.ok) throw new Error(`xAI ${res.status}: ${redact(await res.text(), apiKey).slice(0, 200)}`); const data = (await res.json()) as { choices: Array<{ message?: { content?: string } }>; model: string;