From 293bd6e2c46b857158740dd67716ffaf382e99d5 Mon Sep 17 00:00:00 2001 From: Sunday Labs --labrat Date: Fri, 18 Sep 2026 10:25:14 -0700 Subject: [PATCH] feat(provider): add AnonRouter --- src/main/provider/defaults.ts | 15 ++++++ src/main/provider/providerRegistry.ts | 7 +++ .../src/assets/llm-icons/anonrouter.svg | 23 +++++++++ .../src/components/icons/modelIconRegistry.ts | 5 +- .../provider/basicApiKeyProviders.test.ts | 50 +++++++++++++++++++ test/main/provider/defaultProviders.test.ts | 19 +++++++ 6 files changed, 118 insertions(+), 1 deletion(-) create mode 100644 src/renderer/src/assets/llm-icons/anonrouter.svg diff --git a/src/main/provider/defaults.ts b/src/main/provider/defaults.ts index 3aa9965f7..f7f626158 100644 --- a/src/main/provider/defaults.ts +++ b/src/main/provider/defaults.ts @@ -429,6 +429,21 @@ export const DEFAULT_PROVIDERS: LLM_PROVIDER_BASE[] = [ defaultBaseUrl: 'https://api.anthropic.com' } }, + { + id: 'anonrouter', + name: 'AnonRouter', + apiType: 'openai-completions', + apiKey: '', + baseUrl: 'https://api.anonrouter.ai/v1', + enable: false, + websites: { + official: 'https://anonrouter.ai/', + apiKey: 'https://anonrouter.ai/home/api-keys', + docs: 'https://docs.anonrouter.ai/quickstart', + models: 'https://anonrouter.ai/models', + defaultBaseUrl: 'https://api.anonrouter.ai/v1' + } + }, { id: 'openrouter', name: 'OpenRouter', diff --git a/src/main/provider/providerRegistry.ts b/src/main/provider/providerRegistry.ts index f59d4ec81..ca759073c 100644 --- a/src/main/provider/providerRegistry.ts +++ b/src/main/provider/providerRegistry.ts @@ -131,6 +131,13 @@ const PROVIDER_ID_REGISTRY = new Map([ } }) ], + [ + 'anonrouter', + createDefinition({ + ...OPENAI_BASE, + credentialStrategy: 'api-key' + }) + ], [ 'cheaper-inference', createDefinition({ diff --git a/src/renderer/src/assets/llm-icons/anonrouter.svg b/src/renderer/src/assets/llm-icons/anonrouter.svg new file mode 100644 index 000000000..b6a7f0564 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/anonrouter.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/renderer/src/components/icons/modelIconRegistry.ts b/src/renderer/src/components/icons/modelIconRegistry.ts index 4481fa269..fa2e62850 100644 --- a/src/renderer/src/components/icons/modelIconRegistry.ts +++ b/src/renderer/src/components/icons/modelIconRegistry.ts @@ -88,6 +88,7 @@ import astraflowIcon from '@/assets/llm-icons/astraflow.png?url' import apimartIcon from '@/assets/llm-icons/apimart.ico?url' import apiRouteIcon from '@/assets/llm-icons/api-route.svg?url' import cheaperInferenceIcon from '@/assets/llm-icons/cheaper-inference.svg?url' +import anonrouterIcon from '@/assets/llm-icons/anonrouter.svg?url' export const modelIcons = { 'kimi-for-coding': kimiColorIcon, @@ -108,6 +109,7 @@ export const modelIcons = { 'alibaba-token-plan-cn': dashscopeColorIcon, alibaba: dashscopeColorIcon, aihubmix: aihubmixColorIcon, + anonrouter: anonrouterIcon, 'api-route': apiRouteIcon, apimart: apimartIcon, 'cheaper-inference': cheaperInferenceIcon, @@ -256,7 +258,8 @@ const monoIconUrls = new Set([ voiceAiColorIcon, novitaAiIcon, amdIcon, - apimartIcon + apimartIcon, + anonrouterIcon ]) export const isMonoModelIconUrl = (iconUrl: string): boolean => monoIconUrls.has(iconUrl) diff --git a/test/main/provider/basicApiKeyProviders.test.ts b/test/main/provider/basicApiKeyProviders.test.ts index 7814cdfba..0b0aa34a8 100644 --- a/test/main/provider/basicApiKeyProviders.test.ts +++ b/test/main/provider/basicApiKeyProviders.test.ts @@ -159,6 +159,56 @@ describe('basic API-key provider registrations', () => { }) }) + it('discovers AnonRouter models over authenticated OpenAI-compatible discovery', async () => { + const defaults = DEFAULT_PROVIDERS.find((provider) => provider.id === 'anonrouter')! + expect(defaults).toBeDefined() + const fetchMock = vi.fn().mockImplementation(async () => + Response.json({ + object: 'list', + data: [ + { id: 'meta-llama/llama-3.3-70b', object: 'model', owned_by: 'tinfoil' }, + { id: 'openai/gpt-oss-120b', object: 'model', owned_by: 'venice' } + ] + }) + ) + global.fetch = fetchMock as unknown as typeof fetch + + const config = { ...defaults, apiKey: 'test-key' } + const provider = new AiSdkProvider(config, createProviderSettings()) + expect(resolveAiSdkProviderDefinition(config)).toMatchObject({ + runtimeKind: 'openai-compatible', + modelSource: 'openai', + checkStrategy: 'fetch-models', + credentialStrategy: 'api-key', + routeStrategy: 'none', + embeddingStrategy: 'openai' + }) + + // Nested creator/model ids must survive discovery unchanged. + await expect(provider.fetchModels({ suppressErrors: false })).resolves.toEqual([ + expect.objectContaining({ + id: 'meta-llama/llama-3.3-70b', + name: 'meta-llama/llama-3.3-70b', + providerId: 'anonrouter', + ownedBy: 'tinfoil' + }), + expect.objectContaining({ + id: 'openai/gpt-oss-120b', + providerId: 'anonrouter', + ownedBy: 'venice' + }) + ]) + await expect(provider.check()).resolves.toEqual({ isOk: true, errorMsg: null }) + expect(fetchMock).toHaveBeenCalledWith( + 'https://api.anonrouter.ai/v1/models', + expect.objectContaining({ + method: 'GET', + headers: expect.objectContaining({ Authorization: 'Bearer test-key' }) + }) + ) + expect(mockRunAiSdkGenerateText).not.toHaveBeenCalled() + }) + it('resolves Cheaper Inference through authenticated OpenAI-compatible model discovery', () => { expect( resolveAiSdkProviderDefinition( diff --git a/test/main/provider/defaultProviders.test.ts b/test/main/provider/defaultProviders.test.ts index 8de122d7b..43166b567 100644 --- a/test/main/provider/defaultProviders.test.ts +++ b/test/main/provider/defaultProviders.test.ts @@ -2,6 +2,25 @@ import { describe, expect, it } from 'vitest' import { DEFAULT_PROVIDERS } from '../../../src/main/provider/defaults' describe('DEFAULT_PROVIDERS', () => { + it('includes AnonRouter as a disabled built-in OpenAI-compatible provider', () => { + expect(DEFAULT_PROVIDERS).toContainEqual( + expect.objectContaining({ + id: 'anonrouter', + name: 'AnonRouter', + apiType: 'openai-completions', + baseUrl: 'https://api.anonrouter.ai/v1', + enable: false, + websites: expect.objectContaining({ + official: 'https://anonrouter.ai/', + apiKey: 'https://anonrouter.ai/home/api-keys', + docs: 'https://docs.anonrouter.ai/quickstart', + models: 'https://anonrouter.ai/models', + defaultBaseUrl: 'https://api.anonrouter.ai/v1' + }) + }) + ) + }) + it('includes Cheaper Inference as a disabled built-in OpenAI-compatible provider', () => { expect(DEFAULT_PROVIDERS).toContainEqual( expect.objectContaining({