From c36fb8375d9774bd68ed607b039717aa3bf9d2d2 Mon Sep 17 00:00:00 2001 From: Tony Davis Date: Wed, 28 Oct 2026 10:55:51 -0400 Subject: [PATCH 1/2] feat(provider): add Obscurify.ai Add Obscurify.ai as a disabled built-in OpenAI-compatible provider (https://obscurify.ai/v1). Model discovery reads context_length and the tools flag from its public /v1/models catalog; because the catalog is public, connection checks generate text with meta-llama/llama-3.3-70b-instruct. Embeddings are off. Adds the icon, README grid entries, the provider-runtime spec section and tests. --- README.jp.md | 6 ++ README.md | 6 ++ README.zh.md | 6 ++ docs/features/provider-runtime/spec.md | 10 +++ src/main/provider/defaults.ts | 14 +++ src/main/provider/providerRegistry.ts | 15 ++++ src/main/provider/providers/aiSdkProvider.ts | 29 +++++++ .../src/assets/llm-icons/obscurify.svg | 1 + .../src/components/icons/modelIconRegistry.ts | 3 + .../provider/basicApiKeyProviders.test.ts | 86 +++++++++++++++++++ test/main/provider/defaultProviders.test.ts | 19 ++++ 11 files changed, 195 insertions(+) create mode 100644 src/renderer/src/assets/llm-icons/obscurify.svg diff --git a/README.jp.md b/README.jp.md index b5915c808..6e436231f 100644 --- a/README.jp.md +++ b/README.jp.md @@ -473,6 +473,12 @@ DeepChatはメッセージアプリからリモート操作できるため、デ cheapaimodel + + + Obscurify.ai Icon
+ Obscurify.ai + + diff --git a/README.md b/README.md index 1752088bc..37d7c51f3 100644 --- a/README.md +++ b/README.md @@ -476,6 +476,12 @@ Common commands include `/start`, `/help`, `/pair`, `/new`, `/sessions`, `/use`, cheapaimodel + + + Obscurify.ai Icon
+ Obscurify.ai + + diff --git a/README.zh.md b/README.zh.md index c4c0dfecd..382995679 100644 --- a/README.zh.md +++ b/README.zh.md @@ -470,6 +470,12 @@ DeepChat 可以通过聊天软件远程控制,让你离开桌面后也能继 cheapaimodel + + + Obscurify.ai Icon
+ Obscurify.ai + + diff --git a/docs/features/provider-runtime/spec.md b/docs/features/provider-runtime/spec.md index 870c1a63c..57de976cb 100644 --- a/docs/features/provider-runtime/spec.md +++ b/docs/features/provider-runtime/spec.md @@ -116,6 +116,16 @@ Embeddings are disabled for this profile. Upstream reference: [API docs](https://cloudgpu.app/api/docs?utm_source=deepchat). +## Obscurify.ai + +`obscurify` uses the OpenAI-compatible transport with API-key credentials. Live model discovery +lists every model from the public `/v1/models` catalog and reads its `context_length` and `tools` +flag. The catalog is public, so connection checks generate text with +`meta-llama/llama-3.3-70b-instruct` and the shared 16-token output limit to validate credentials. +Embeddings are disabled for this profile. + +Upstream reference: [API docs](https://obscurify.ai/api-docs). + ## OpenAI Codex `openai-codex` is separate from standard `openai`: distinct provider ID, runtime kind, credential store, diff --git a/src/main/provider/defaults.ts b/src/main/provider/defaults.ts index 9bf998ef7..0b25da295 100644 --- a/src/main/provider/defaults.ts +++ b/src/main/provider/defaults.ts @@ -720,6 +720,20 @@ const PROVIDER_DEFAULTS: LLM_PROVIDER_BASE[] = [ models: 'https://cheapaimodel.ai/models?utm_source=deepchat' } }, + { + id: 'obscurify', + name: 'Obscurify.ai', + apiType: 'openai-completions', + apiKey: '', + baseUrl: 'https://obscurify.ai/v1', + enable: false, + websites: { + official: 'https://obscurify.ai/?utm_source=deepchat', + apiKey: 'https://obscurify.ai/account', + docs: 'https://obscurify.ai/api-docs', + models: 'https://obscurify.ai/model_cards' + } + }, { id: 'aistupidlevel', name: 'AI Stupid Level', diff --git a/src/main/provider/providerRegistry.ts b/src/main/provider/providerRegistry.ts index 2b788154b..e3bba4837 100644 --- a/src/main/provider/providerRegistry.ts +++ b/src/main/provider/providerRegistry.ts @@ -14,6 +14,7 @@ export type AiSdkModelSourceStrategy = | 'apimodels' | 'runcrate' | 'cloudgpu' + | 'obscurify' | 'apimart' | 'openai-codex' | 'opencode-go' @@ -664,6 +665,20 @@ const PROVIDER_ID_REGISTRY = new Map([ checkMaxTokens: 16 }) ], + [ + 'obscurify', + createDefinition({ + ...OPENAI_BASE, + modelSource: 'obscurify', + checkStrategy: 'generate-text', + credentialStrategy: 'api-key', + embeddingStrategy: 'none', + checkModelId: 'meta-llama/llama-3.3-70b-instruct', + checkPrompt: 'Hello', + checkTemperature: 0.2, + checkMaxTokens: 16 + }) + ], [ 'poe', createDefinition({ diff --git a/src/main/provider/providers/aiSdkProvider.ts b/src/main/provider/providers/aiSdkProvider.ts index 0eabc0284..3ed3a9b6a 100644 --- a/src/main/provider/providers/aiSdkProvider.ts +++ b/src/main/provider/providers/aiSdkProvider.ts @@ -1786,6 +1786,8 @@ export class AiSdkProvider extends BaseLLMProvider { return this.fetchRuncrateModels() case 'cloudgpu': return this.fetchCloudgpuModels() + case 'obscurify': + return this.fetchObscurifyModels() case 'bedrock': return this.fetchBedrockModels() case 'new-api': @@ -2222,6 +2224,33 @@ export class AiSdkProvider extends BaseLLMProvider { return models } + /** + * Obscurify.ai's public catalog answers without a key and publishes each model's context length + * and whether it accepts tools. + */ + private async fetchObscurifyModels(): Promise { + const records = await this.fetchOpenAIModelRecords({ timeout: this.getModelFetchTimeout() }) + const models: MODEL_META[] = [] + for (const model of records) { + const modelId = typeof model.id === 'string' ? model.id.trim() : '' + if (!modelId) { + continue + } + const contextLength = toPositiveFiniteNumber(model.context_length) + models.push({ + id: modelId, + name: modelId, + group: 'default', + providerId: this.provider.id, + isCustom: false, + type: ModelType.Chat, + ...(contextLength !== undefined ? { contextLength } : {}), + ...(typeof model.tools === 'boolean' ? { functionCall: model.tools } : {}) + }) + } + return models + } + private async fetchBedrockModels(): Promise { const provider = this.provider as AWS_BEDROCK_PROVIDER const credential = provider.credential diff --git a/src/renderer/src/assets/llm-icons/obscurify.svg b/src/renderer/src/assets/llm-icons/obscurify.svg new file mode 100644 index 000000000..f2e50841e --- /dev/null +++ b/src/renderer/src/assets/llm-icons/obscurify.svg @@ -0,0 +1 @@ + diff --git a/src/renderer/src/components/icons/modelIconRegistry.ts b/src/renderer/src/components/icons/modelIconRegistry.ts index 9848054d8..bbcfc5e9f 100644 --- a/src/renderer/src/components/icons/modelIconRegistry.ts +++ b/src/renderer/src/components/icons/modelIconRegistry.ts @@ -57,6 +57,7 @@ import aistupidlevelIcon from '@/assets/llm-icons/aistupidlevel.svg?url' import runcrateIcon from '@/assets/llm-icons/runcrate.svg?url' import cloudgpuIcon from '@/assets/llm-icons/cloudgpu.svg?url' import cheapaimodelIcon from '@/assets/llm-icons/cheapaimodel.svg?url' +import obscurifyIcon from '@/assets/llm-icons/obscurify.svg?url' import demonrouteColorIcon from '@/assets/llm-icons/demonroute.svg?url' import orcarouterColorIcon from '@/assets/llm-icons/orcarouter.svg?url' import synthoraiColorIcon from '@/assets/llm-icons/synthorai.svg?url' @@ -210,6 +211,7 @@ export const modelIcons = { runcrate: runcrateIcon, cloudgpu: cloudgpuIcon, cheapaimodel: cheapaimodelIcon, + obscurify: obscurifyIcon, demonroute: demonrouteColorIcon, orcarouter: orcarouterColorIcon, synthorai: synthoraiColorIcon, @@ -296,6 +298,7 @@ const monoIconUrls = new Set([ cloudgpuIcon, xxapiIcon, llmbuzzIcon, + obscurifyIcon, demonrouteColorIcon, githubColorIcon, qiniuIcon, diff --git a/test/main/provider/basicApiKeyProviders.test.ts b/test/main/provider/basicApiKeyProviders.test.ts index 78b93d80c..78f84dc18 100644 --- a/test/main/provider/basicApiKeyProviders.test.ts +++ b/test/main/provider/basicApiKeyProviders.test.ts @@ -1866,6 +1866,92 @@ describe('basic API-key provider registrations', () => { ) }) + it('checks Obscurify.ai with a chat model instead of its public model list', async () => { + const defaults = DEFAULT_PROVIDERS.find((provider) => provider.id === 'obscurify')! + expect(defaults).toBeDefined() + const config = { ...defaults, apiKey: 'test-key' } + // The /v1/models catalog is public, so listing models cannot validate a key. + expect(resolveAiSdkProviderDefinition(config)).toMatchObject({ + runtimeKind: 'openai-compatible', + modelSource: 'obscurify', + checkStrategy: 'generate-text', + credentialStrategy: 'api-key', + embeddingStrategy: 'none', + checkModelId: 'meta-llama/llama-3.3-70b-instruct' + }) + const provider = new AiSdkProvider(config, createProviderSettings()) + ;(provider as any).isInitialized = true + + await expect(provider.check()).resolves.toEqual({ isOk: true, errorMsg: null }) + expect(mockRunAiSdkGenerateText).toHaveBeenCalledWith( + expect.objectContaining({ + providerKind: 'openai-compatible', + provider: expect.objectContaining({ + id: 'obscurify', + apiKey: 'test-key', + baseUrl: 'https://obscurify.ai/v1' + }) + }), + [{ role: 'user', content: 'Hello' }], + 'meta-llama/llama-3.3-70b-instruct', + expect.any(Object), + 0.2, + 16 + ) + }) + + it('reads context length and tool support from the Obscurify.ai catalog', async () => { + const defaults = DEFAULT_PROVIDERS.find(({ id }) => id === 'obscurify')! + const fetchMock = vi.fn().mockResolvedValue( + Response.json({ + object: 'list', + data: [ + { id: 'moonshotai/kimi-k2-0905', context_length: 262144, tools: true }, + { id: 'vendor/plain-model', context_length: 0, tools: false }, + { id: 'vendor/no-metadata' }, + { id: ' ' }, + { id: 7 } + ] + }) + ) + global.fetch = fetchMock + const provider = new AiSdkProvider(defaults, createProviderSettings()) + + expect(await provider.fetchModels({ suppressErrors: false })).toEqual([ + { + id: 'moonshotai/kimi-k2-0905', + name: 'moonshotai/kimi-k2-0905', + group: 'default', + providerId: 'obscurify', + isCustom: false, + type: 'chat', + contextLength: 262144, + functionCall: true + }, + { + id: 'vendor/plain-model', + name: 'vendor/plain-model', + group: 'default', + providerId: 'obscurify', + isCustom: false, + type: 'chat', + functionCall: false + }, + { + id: 'vendor/no-metadata', + name: 'vendor/no-metadata', + group: 'default', + providerId: 'obscurify', + isCustom: false, + type: 'chat' + } + ]) + expect(fetchMock).toHaveBeenCalledWith( + 'https://obscurify.ai/v1/models', + expect.objectContaining({ method: 'GET' }) + ) + }) + it('lists only text chat models from the public Runcrate catalog', async () => { const defaults = DEFAULT_PROVIDERS.find(({ id }) => id === 'runcrate')! const fetchMock = vi.fn().mockResolvedValue( diff --git a/test/main/provider/defaultProviders.test.ts b/test/main/provider/defaultProviders.test.ts index e6b4191b5..7fd2e1256 100644 --- a/test/main/provider/defaultProviders.test.ts +++ b/test/main/provider/defaultProviders.test.ts @@ -170,6 +170,25 @@ describe('DEFAULT_PROVIDERS', () => { ) }) + it('includes Obscurify.ai as a disabled built-in OpenAI-compatible provider', () => { + expect(DEFAULT_PROVIDERS).toContainEqual( + expect.objectContaining({ + id: 'obscurify', + name: 'Obscurify.ai', + apiType: 'openai-completions', + baseUrl: 'https://obscurify.ai/v1', + enable: false, + websites: expect.objectContaining({ + official: 'https://obscurify.ai/?utm_source=deepchat', + apiKey: 'https://obscurify.ai/account', + docs: 'https://obscurify.ai/api-docs', + models: 'https://obscurify.ai/model_cards', + defaultBaseUrl: 'https://obscurify.ai/v1' + }) + }) + ) + }) + it('includes AI Stupid Level as a disabled built-in OpenAI-compatible provider', () => { expect(DEFAULT_PROVIDERS).toContainEqual( expect.objectContaining({ From 0cbfbcd750b998acc62b1203f9cb1ee8e2f46a35 Mon Sep 17 00:00:00 2001 From: Tony Davis Date: Thu, 29 Oct 2026 09:48:14 -0400 Subject: [PATCH 2/2] fix(provider): leave non-chat models out of the Obscurify.ai list Skip decision models (decisions: true, including the typesafe/jev-* ids matched by isJevJudgmentModelId) and the image model. The id filter is shared with CloudGPU. --- docs/features/provider-runtime/spec.md | 6 ++- src/main/provider/providers/aiSdkProvider.ts | 47 ++++++++++++------- .../provider/basicApiKeyProviders.test.ts | 6 ++- 3 files changed, 40 insertions(+), 19 deletions(-) diff --git a/docs/features/provider-runtime/spec.md b/docs/features/provider-runtime/spec.md index 57de976cb..2a6bc68a8 100644 --- a/docs/features/provider-runtime/spec.md +++ b/docs/features/provider-runtime/spec.md @@ -119,8 +119,10 @@ Upstream reference: [API docs](https://cloudgpu.app/api/docs?utm_source=deepchat ## Obscurify.ai `obscurify` uses the OpenAI-compatible transport with API-key credentials. Live model discovery -lists every model from the public `/v1/models` catalog and reads its `context_length` and `tools` -flag. The catalog is public, so connection checks generate text with +reads the public `/v1/models` catalog, keeps each model's `context_length` and `tools` flag, and +skips entries that are not chat models: decision models (`decisions: true`, which include the +`typesafe/jev-*` judgment models) and ids that match the shared non-chat filter, such as image +models. The catalog is public, so connection checks generate text with `meta-llama/llama-3.3-70b-instruct` and the shared 16-token output limit to validate credentials. Embeddings are disabled for this profile. diff --git a/src/main/provider/providers/aiSdkProvider.ts b/src/main/provider/providers/aiSdkProvider.ts index 3ed3a9b6a..1b0ec0e4e 100644 --- a/src/main/provider/providers/aiSdkProvider.ts +++ b/src/main/provider/providers/aiSdkProvider.ts @@ -9,6 +9,7 @@ import { resolveNewApiEndpointTypeFromRoute, type NewApiEndpointType } from '@shared/model' +import { isJevJudgmentModelId } from '@shared/jevProtocol' import { isTtsModelConfig, isTtsModelId } from '@shared/ttsSettings' import { isVideoGenerationModelConfig } from '@shared/videoGenerationSettings' import type { ChatMessage } from '@shared/types/core/chat-message' @@ -212,6 +213,26 @@ function toPositiveFiniteNumber(value: unknown): number | undefined { return typeof value === 'number' && Number.isFinite(value) && value > 0 ? value : undefined } +// Id fragments that mark a catalog entry as clearly not a chat model, for public catalogs that +// carry no capability field (embedding, rerank, image, audio, speech). +const NON_CHAT_MODEL_ID_PATTERNS = [ + 'embed', + 'rerank', + 'image', + 'flux-', + 'audio', + 'speech', + 'tts', + 'whisper', + '-asr', + 'transcri' +] + +function isNonChatCatalogModelId(modelId: string): boolean { + const lower = modelId.toLowerCase() + return NON_CHAT_MODEL_ID_PATTERNS.some((pattern) => lower.includes(pattern)) +} + function toErrorMessage(error: unknown, fallback: string): string { if (error instanceof Error && error.message) { return error.message @@ -2190,23 +2211,10 @@ export class AiSdkProvider extends BaseLLMProvider { */ private async fetchCloudgpuModels(): Promise { const records = await this.fetchOpenAIModelRecords({ timeout: this.getModelFetchTimeout() }) - const nonChatPatterns = [ - 'embed', - 'rerank', - 'image', - 'flux-', - 'audio', - 'speech', - 'tts', - 'whisper', - '-asr', - 'transcri' - ] const models: MODEL_META[] = [] for (const model of records) { const modelId = typeof model.id === 'string' ? model.id.trim() : '' - const lower = modelId.toLowerCase() - if (!modelId || nonChatPatterns.some((pattern) => lower.includes(pattern))) { + if (!modelId || isNonChatCatalogModelId(modelId)) { continue } models.push({ @@ -2226,14 +2234,21 @@ export class AiSdkProvider extends BaseLLMProvider { /** * Obscurify.ai's public catalog answers without a key and publishes each model's context length - * and whether it accepts tools. + * and whether it accepts tools. It also lists models that are not chat models: decision models + * (flagged `decisions: true`, which include the Jev judgment models) and an image model. Those are + * left out so they cannot be picked in chat. */ private async fetchObscurifyModels(): Promise { const records = await this.fetchOpenAIModelRecords({ timeout: this.getModelFetchTimeout() }) const models: MODEL_META[] = [] for (const model of records) { const modelId = typeof model.id === 'string' ? model.id.trim() : '' - if (!modelId) { + if ( + !modelId || + model.decisions === true || + isJevJudgmentModelId(modelId) || + isNonChatCatalogModelId(modelId) + ) { continue } const contextLength = toPositiveFiniteNumber(model.context_length) diff --git a/test/main/provider/basicApiKeyProviders.test.ts b/test/main/provider/basicApiKeyProviders.test.ts index 78f84dc18..6cbabc8c6 100644 --- a/test/main/provider/basicApiKeyProviders.test.ts +++ b/test/main/provider/basicApiKeyProviders.test.ts @@ -1900,7 +1900,7 @@ describe('basic API-key provider registrations', () => { ) }) - it('reads context length and tool support from the Obscurify.ai catalog', async () => { + it('reads chat models with context length and tool support from the Obscurify.ai catalog', async () => { const defaults = DEFAULT_PROVIDERS.find(({ id }) => id === 'obscurify')! const fetchMock = vi.fn().mockResolvedValue( Response.json({ @@ -1909,6 +1909,10 @@ describe('basic API-key provider registrations', () => { { id: 'moonshotai/kimi-k2-0905', context_length: 262144, tools: true }, { id: 'vendor/plain-model', context_length: 0, tools: false }, { id: 'vendor/no-metadata' }, + { id: 'google/gemini-2.5-flash-image', context_length: 32768, tools: false }, + { id: 'typesafe/jev-1.14', decisions: true, tools: false }, + { id: 'typesafe/jev-latest', tools: false }, + { id: 'vendor/decider-small', decisions: true, tools: false }, { id: ' ' }, { id: 7 } ]