feat(proxy): favourite-index tool presentation #86
@@ -101,6 +101,15 @@ export class AnthropicProvider implements LlmProvider {
|
||||
res.on('data', (chunk: Buffer) => chunks.push(chunk));
|
||||
res.on('end', () => {
|
||||
const raw = Buffer.concat(chunks).toString('utf-8');
|
||||
// Surface transport failures (e.g. a 404 "model not found" or a 401
|
||||
// for an OAuth-gated key) instead of parsing the error body as a
|
||||
// completion — otherwise it slips through as an empty completion and
|
||||
// the gate reports the misleading "did not contain valid JSON".
|
||||
const status = res.statusCode ?? 0;
|
||||
if (status >= 400) {
|
||||
reject(new Error(`Anthropic HTTP ${status}: ${raw.slice(0, 300)}`));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
resolve(JSON.parse(raw));
|
||||
} catch {
|
||||
|
||||
51
src/mcplocal/tests/anthropic-provider.test.ts
Normal file
51
src/mcplocal/tests/anthropic-provider.test.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* AnthropicProvider — transport-error surfacing (parity with OpenAiProvider).
|
||||
*
|
||||
* A 404 "model not found" (OAuth-gated key on an unavailable model) or a 401
|
||||
* must reject with the status + body, not parse the error body as an empty
|
||||
* completion (which the gate then misreports as "did not contain valid JSON").
|
||||
*/
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
|
||||
let mockStatus = 200;
|
||||
let mockBody = JSON.stringify({ content: [{ type: 'text', text: 'ok' }], usage: {}, stop_reason: 'end_turn' });
|
||||
|
||||
vi.mock('node:https', () => {
|
||||
const request = vi.fn((_opts: unknown, cb?: unknown) => {
|
||||
const res = {
|
||||
statusCode: mockStatus,
|
||||
on: (event: string, handler: (d?: unknown) => void) => {
|
||||
if (event === 'data') handler(Buffer.from(mockBody));
|
||||
if (event === 'end') handler();
|
||||
return res;
|
||||
},
|
||||
};
|
||||
if (typeof cb === 'function') (cb as (r: unknown) => void)(res);
|
||||
return { on: vi.fn().mockReturnThis(), write: vi.fn(), end: vi.fn() };
|
||||
});
|
||||
return { default: { request }, request };
|
||||
});
|
||||
|
||||
const { AnthropicProvider } = await import('../src/providers/anthropic.js');
|
||||
|
||||
describe('AnthropicProvider transport handling', () => {
|
||||
beforeEach(() => {
|
||||
mockStatus = 200;
|
||||
mockBody = JSON.stringify({ content: [{ type: 'text', text: 'ok' }], usage: {}, stop_reason: 'end_turn' });
|
||||
});
|
||||
|
||||
it('rejects on HTTP 404 (model not found) instead of masking it as bad JSON', async () => {
|
||||
mockStatus = 404;
|
||||
mockBody = JSON.stringify({ type: 'error', error: { type: 'not_found_error', message: 'model: claude-opus-4' } });
|
||||
const p = new AnthropicProvider({ apiKey: 'sk-ant-oat01-x' });
|
||||
await expect(
|
||||
p.complete({ messages: [{ role: 'user', content: 'hi' }], maxTokens: 8 }),
|
||||
).rejects.toThrow(/Anthropic HTTP 404/);
|
||||
});
|
||||
|
||||
it('still returns the completion on 200', async () => {
|
||||
const p = new AnthropicProvider({ apiKey: 'sk-ant-oat01-x' });
|
||||
const r = await p.complete({ messages: [{ role: 'user', content: 'hi' }], maxTokens: 8 });
|
||||
expect(r.content).toBe('ok');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user