mirror of
https://github.com/amitwh/markdown-converter.git
synced 2026-10-01 09:19:34 +05:30
feat(ai): add Anthropic-compatible provider; fix async plugin backends
Anthropic-compatible provider: - New 'anthropic-compatible' option for any base URL speaking the Anthropic messages schema (LiteLLM proxies, Bedrock gateways, local servers) - Sends x-api-key AND Bearer auth when a key is set (gateway-friendly, harmless for the official API); keyless proxies supported - Tolerates base URLs with or without a trailing /v1 segment - Settings modal, manifest, and provider docs updated Runtime bug fixes found by booting the app (run-to-verify pass): - PDF editor: File > Open PDF sends operation=null which matched no switch case and crashed on getElementById(undefined); now defaults to the merge section - backlinks-panel: wrong require depth (../../utils -> ../utils) threw at panel registration time - writing-studio stack was written against a synchronous settings backend but the real one is IPC-backed: GoalTracker/SnapshotManager/ProjectManager and all four panels now await; JSON.parse(Promise) crashes eliminated - manuscript panel used window.prompt (unavailable in Electron); replaced with an inline dialog - collaboration comment-store/save-load made async to match its IPC IO 617/617 tests green; 4 consecutive clean app boots (no uncaught errors)
This commit is contained in:
@@ -33,6 +33,10 @@ describe('AiProviders', () => {
|
||||
it('does not require an API key for local providers', () => {
|
||||
expect(() => resolveSettings({ provider: 'lmstudio' })).not.toThrow();
|
||||
expect(() => resolveSettings({ provider: 'ollama' })).not.toThrow();
|
||||
// Self-hosted Anthropic-compatible proxies may front their own auth
|
||||
expect(() =>
|
||||
resolveSettings({ provider: 'anthropic-compatible', baseUrl: 'http://localhost:4000' })
|
||||
).not.toThrow();
|
||||
});
|
||||
|
||||
it('requires an API key for remote providers', () => {
|
||||
@@ -109,6 +113,52 @@ describe('AiProviders', () => {
|
||||
expect(body.system).toBe('be nice');
|
||||
});
|
||||
|
||||
it('routes anthropic-compatible to the custom base with Bearer + x-api-key auth', async () => {
|
||||
const fetchImpl = okFetch({ content: [{ type: 'text', text: 'proxy ok' }] });
|
||||
const result = await complete(
|
||||
{
|
||||
provider: 'anthropic-compatible',
|
||||
baseUrl: 'http://localhost:4000',
|
||||
apiKey: 'proxy-key',
|
||||
messages,
|
||||
},
|
||||
{ fetchImpl }
|
||||
);
|
||||
|
||||
expect(result.content).toBe('proxy ok');
|
||||
const [url, init] = fetchImpl.mock.calls[0];
|
||||
// Base without a /v1 suffix gets one appended
|
||||
expect(url).toBe('http://localhost:4000/v1/messages');
|
||||
// Both auth styles are sent so LiteLLM/Bedrock-style gateways accept it
|
||||
expect(init.headers['x-api-key']).toBe('proxy-key');
|
||||
expect(init.headers.Authorization).toBe('Bearer proxy-key');
|
||||
expect(init.headers['anthropic-version']).toBe('2023-06-01');
|
||||
});
|
||||
|
||||
it('does not double the /v1 segment when the base already ends with it', async () => {
|
||||
const fetchImpl = okFetch({ content: [{ type: 'text', text: 'ok' }] });
|
||||
await complete(
|
||||
{
|
||||
provider: 'anthropic-compatible',
|
||||
baseUrl: 'http://localhost:4000/v1/',
|
||||
messages,
|
||||
},
|
||||
{ fetchImpl }
|
||||
);
|
||||
// Trailing slash is stripped by resolveSettings, so the base ends in /v1
|
||||
const [url, init] = fetchImpl.mock.calls[0];
|
||||
expect(url).toBe('http://localhost:4000/v1/messages');
|
||||
// Keyless proxies must work without auth headers
|
||||
expect(init.headers.Authorization).toBeUndefined();
|
||||
expect(init.headers['x-api-key']).toBeUndefined();
|
||||
});
|
||||
|
||||
it('rejects anthropic-compatible without a base URL', async () => {
|
||||
await expect(
|
||||
complete({ provider: 'anthropic-compatible', messages }, { fetchImpl: okFetch({}) })
|
||||
).rejects.toThrow(/Invalid API base URL/);
|
||||
});
|
||||
|
||||
it('surfaces HTTP failures as user-safe errors without the body', async () => {
|
||||
const fetchImpl = jest.fn().mockResolvedValue({ ok: false, status: 401 });
|
||||
await expect(
|
||||
|
||||
Reference in New Issue
Block a user