Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.jp.md
Original file line number Diff line number Diff line change
Expand Up @@ -473,6 +473,12 @@ DeepChatはメッセージアプリからリモート操作できるため、デ
<a href="https://cheapaimodel.ai/?utm_source=deepchat">cheapaimodel</a>
</td>
</tr>
<tr align="center">
<td>
<img src="./src/renderer/src/assets/llm-icons/obscurify.svg" width="50" height="50" alt="Obscurify.ai Icon"><br/>
<a href="https://obscurify.ai/?utm_source=deepchat">Obscurify.ai</a>
</td>
</tr>

</table>

Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -476,6 +476,12 @@ Common commands include `/start`, `/help`, `/pair`, `/new`, `/sessions`, `/use`,
<a href="https://cheapaimodel.ai/?utm_source=deepchat">cheapaimodel</a>
</td>
</tr>
<tr align="center">
<td>
<img src="./src/renderer/src/assets/llm-icons/obscurify.svg" width="50" height="50" alt="Obscurify.ai Icon"><br/>
<a href="https://obscurify.ai/?utm_source=deepchat">Obscurify.ai</a>
</td>
</tr>

</table>

Expand Down
6 changes: 6 additions & 0 deletions README.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -470,6 +470,12 @@ DeepChat 可以通过聊天软件远程控制,让你离开桌面后也能继
<a href="https://cheapaimodel.ai/?utm_source=deepchat">cheapaimodel</a>
</td>
</tr>
<tr align="center">
<td>
<img src="./src/renderer/src/assets/llm-icons/obscurify.svg" width="50" height="50" alt="Obscurify.ai Icon"><br/>
<a href="https://obscurify.ai/?utm_source=deepchat">Obscurify.ai</a>
</td>
</tr>

</table>

Expand Down
12 changes: 12 additions & 0 deletions docs/features/provider-runtime/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
14 changes: 14 additions & 0 deletions src/main/provider/defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
15 changes: 15 additions & 0 deletions src/main/provider/providerRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export type AiSdkModelSourceStrategy =
| 'apimodels'
| 'runcrate'
| 'cloudgpu'
| 'obscurify'
| 'apimart'
| 'openai-codex'
| 'opencode-go'
Expand Down Expand Up @@ -664,6 +665,20 @@ const PROVIDER_ID_REGISTRY = new Map<string, AiSdkProviderDefinition>([
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({
Expand Down
72 changes: 58 additions & 14 deletions src/main/provider/providers/aiSdkProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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':
Expand Down Expand Up @@ -2188,23 +2211,10 @@ export class AiSdkProvider extends BaseLLMProvider {
*/
private async fetchCloudgpuModels(): Promise<MODEL_META[]> {
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({
Expand All @@ -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<MODEL_META[]> {
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<MODEL_META[]> {
const provider = this.provider as AWS_BEDROCK_PROVIDER
const credential = provider.credential
Expand Down
1 change: 1 addition & 0 deletions src/renderer/src/assets/llm-icons/obscurify.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions src/renderer/src/components/icons/modelIconRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -210,6 +211,7 @@ export const modelIcons = {
runcrate: runcrateIcon,
cloudgpu: cloudgpuIcon,
cheapaimodel: cheapaimodelIcon,
obscurify: obscurifyIcon,
demonroute: demonrouteColorIcon,
orcarouter: orcarouterColorIcon,
synthorai: synthoraiColorIcon,
Expand Down Expand Up @@ -296,6 +298,7 @@ const monoIconUrls = new Set<string>([
cloudgpuIcon,
xxapiIcon,
llmbuzzIcon,
obscurifyIcon,
demonrouteColorIcon,
githubColorIcon,
qiniuIcon,
Expand Down
90 changes: 90 additions & 0 deletions test/main/provider/basicApiKeyProviders.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Loading