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
+ |
+
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
+ |
+
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
+ |
+
diff --git a/docs/features/provider-runtime/spec.md b/docs/features/provider-runtime/spec.md
index 870c1a63c..2a6bc68a8 100644
--- a/docs/features/provider-runtime/spec.md
+++ b/docs/features/provider-runtime/spec.md
@@ -116,6 +116,18 @@ 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
+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.
+
+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..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
@@ -1786,6 +1807,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':
@@ -2188,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({
@@ -2222,6 +2232,40 @@ 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. 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 ||
+ model.decisions === true ||
+ isJevJudgmentModelId(modelId) ||
+ isNonChatCatalogModelId(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..6cbabc8c6 100644
--- a/test/main/provider/basicApiKeyProviders.test.ts
+++ b/test/main/provider/basicApiKeyProviders.test.ts
@@ -1866,6 +1866,96 @@ 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 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({
+ 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: '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 }
+ ]
+ })
+ )
+ 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({