diff --git a/README.jp.md b/README.jp.md
index 33efe2ee3b..0d9574a125 100644
--- a/README.jp.md
+++ b/README.jp.md
@@ -460,6 +460,10 @@ DeepChatはメッセージアプリからリモート操作できるため、デ

AI Stupid Level
+
+ 
+ Runcrate
+ |
diff --git a/README.md b/README.md
index 689bb80120..cdac50c619 100644
--- a/README.md
+++ b/README.md
@@ -463,6 +463,10 @@ Common commands include `/start`, `/help`, `/pair`, `/new`, `/sessions`, `/use`,

AI Stupid Level
+
+ 
+ Runcrate
+ |
diff --git a/README.zh.md b/README.zh.md
index 7e6cb1ed20..c0d31e190e 100644
--- a/README.zh.md
+++ b/README.zh.md
@@ -457,6 +457,10 @@ DeepChat 可以通过聊天软件远程控制,让你离开桌面后也能继

AI Stupid Level
+
+ 
+ Runcrate
+ |
diff --git a/src/main/provider/defaults.ts b/src/main/provider/defaults.ts
index 51b6f586e1..58c09115f5 100644
--- a/src/main/provider/defaults.ts
+++ b/src/main/provider/defaults.ts
@@ -650,6 +650,20 @@ const PROVIDER_DEFAULTS: LLM_PROVIDER_BASE[] = [
models: 'https://opper.ai/models'
}
},
+ {
+ id: 'runcrate',
+ name: 'Runcrate',
+ apiType: 'openai-completions',
+ apiKey: '',
+ baseUrl: 'https://api.runcrate.ai/v1',
+ enable: false,
+ websites: {
+ official: 'https://www.runcrate.ai/?utm_source=deepchat',
+ apiKey: 'https://www.runcrate.ai/dashboard/api-keys?utm_source=deepchat',
+ docs: 'https://www.runcrate.ai/docs?utm_source=deepchat',
+ models: 'https://www.runcrate.ai/inference?utm_source=deepchat'
+ }
+ },
{
id: 'aistupidlevel',
name: 'AI Stupid Level',
diff --git a/src/main/provider/providerRegistry.ts b/src/main/provider/providerRegistry.ts
index 561e1d1a2e..608aefe28b 100644
--- a/src/main/provider/providerRegistry.ts
+++ b/src/main/provider/providerRegistry.ts
@@ -12,6 +12,7 @@ export type AiSdkBehaviorPreset =
export type AiSdkModelSourceStrategy =
| 'openai'
| 'apimodels'
+ | 'runcrate'
| 'apimart'
| 'openai-codex'
| 'opencode-go'
@@ -611,6 +612,19 @@ const PROVIDER_ID_REGISTRY = new Map([
credentialStrategy: 'api-key'
})
],
+ [
+ 'runcrate',
+ createDefinition({
+ ...OPENAI_BASE,
+ modelSource: 'runcrate',
+ checkStrategy: 'generate-text',
+ credentialStrategy: 'api-key',
+ checkModelId: 'deepseek-ai/DeepSeek-V4.1-Flash',
+ 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 74eda8628b..ea30e7bae6 100644
--- a/src/main/provider/providers/aiSdkProvider.ts
+++ b/src/main/provider/providers/aiSdkProvider.ts
@@ -1782,6 +1782,8 @@ export class AiSdkProvider extends BaseLLMProvider {
}
return models
}
+ case 'runcrate':
+ return this.fetchRuncrateModels()
case 'bedrock':
return this.fetchBedrockModels()
case 'new-api':
@@ -2135,6 +2137,49 @@ export class AiSdkProvider extends BaseLLMProvider {
return models
}
+ /**
+ * Runcrate's public catalog also lists image, video, audio and embedding models.
+ * Keep the text-in, text-out models and read the limits and features it publishes.
+ */
+ private async fetchRuncrateModels(): Promise {
+ const records = await this.fetchOpenAIModelRecords({ timeout: this.getModelFetchTimeout() })
+ const toStrings = (value: unknown): string[] | undefined =>
+ Array.isArray(value)
+ ? value.filter((item): item is string => typeof item === 'string')
+ : undefined
+
+ const models: MODEL_META[] = []
+ for (const model of records) {
+ const modelId = typeof model.id === 'string' ? model.id.trim() : ''
+ const outputModalities = toStrings(model.output_modalities)
+ const inputModalities = toStrings(model.input_modalities)
+ if (!modelId || !outputModalities?.includes('text') || !inputModalities?.includes('text')) {
+ continue
+ }
+ const features = toStrings(model.supported_features)
+ const contextLength = toPositiveFiniteNumber(model.context_length)
+ const maxTokens = toPositiveFiniteNumber(model.max_output_length)
+ models.push({
+ id: modelId,
+ name: modelId,
+ group: 'default',
+ providerId: this.provider.id,
+ isCustom: false,
+ type: ModelType.Chat,
+ ...(contextLength !== undefined ? { contextLength } : {}),
+ ...(maxTokens !== undefined ? { maxTokens } : {}),
+ ...(features
+ ? { functionCall: features.includes('tools'), reasoning: features.includes('reasoning') }
+ : {}),
+ ...(inputModalities ? { vision: inputModalities.includes('image') } : {}),
+ ...(typeof model.owned_by === 'string' && model.owned_by.trim()
+ ? { ownedBy: model.owned_by.trim() }
+ : {})
+ })
+ }
+ 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/runcrate.svg b/src/renderer/src/assets/llm-icons/runcrate.svg
new file mode 100644
index 0000000000..6d083c98f5
--- /dev/null
+++ b/src/renderer/src/assets/llm-icons/runcrate.svg
@@ -0,0 +1 @@
+
diff --git a/src/renderer/src/components/icons/modelIconRegistry.ts b/src/renderer/src/components/icons/modelIconRegistry.ts
index b62ab381cc..80dbe4dad4 100644
--- a/src/renderer/src/components/icons/modelIconRegistry.ts
+++ b/src/renderer/src/components/icons/modelIconRegistry.ts
@@ -54,6 +54,7 @@ import requestyColorIcon from '@/assets/llm-icons/requesty.png?url'
import litellmColorIcon from '@/assets/llm-icons/litellm.svg?url'
import opperColorIcon from '@/assets/llm-icons/opper.svg?url'
import aistupidlevelIcon from '@/assets/llm-icons/aistupidlevel.svg?url'
+import runcrateIcon from '@/assets/llm-icons/runcrate.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'
@@ -200,6 +201,7 @@ export const modelIcons = {
litellm: litellmColorIcon,
opper: opperColorIcon,
aistupidlevel: aistupidlevelIcon,
+ runcrate: runcrateIcon,
demonroute: demonrouteColorIcon,
orcarouter: orcarouterColorIcon,
synthorai: synthoraiColorIcon,
@@ -282,6 +284,7 @@ const monoIconUrls = new Set([
atlascloudIcon,
opperColorIcon,
aistupidlevelIcon,
+ runcrateIcon,
demonrouteColorIcon,
githubColorIcon,
qiniuIcon,
diff --git a/test/main/provider/basicApiKeyProviders.test.ts b/test/main/provider/basicApiKeyProviders.test.ts
index 9fba0738a5..175615c496 100644
--- a/test/main/provider/basicApiKeyProviders.test.ts
+++ b/test/main/provider/basicApiKeyProviders.test.ts
@@ -1689,6 +1689,91 @@ describe('basic API-key provider registrations', () => {
)
})
+ it('checks Runcrate with a chat model instead of its public model list', async () => {
+ const defaults = DEFAULT_PROVIDERS.find((provider) => provider.id === 'runcrate')!
+ expect(defaults).toBeDefined()
+ const config = { ...defaults, apiKey: 'test-key' }
+ // Runcrate's /v1/models answers without a key, so listing models cannot validate one.
+ expect(resolveAiSdkProviderDefinition(config)).toMatchObject({
+ runtimeKind: 'openai-compatible',
+ modelSource: 'runcrate',
+ checkStrategy: 'generate-text',
+ credentialStrategy: 'api-key',
+ checkModelId: 'deepseek-ai/DeepSeek-V4.1-Flash'
+ })
+ 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: 'runcrate',
+ apiKey: 'test-key',
+ baseUrl: 'https://api.runcrate.ai/v1'
+ })
+ }),
+ [{ role: 'user', content: 'Hello' }],
+ 'deepseek-ai/DeepSeek-V4.1-Flash',
+ expect.any(Object),
+ 0.2,
+ 16
+ )
+ })
+
+ 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(
+ Response.json({
+ object: 'list',
+ data: [
+ {
+ id: 'Qwen/Qwen3-Next-80B-A3B-Instruct',
+ owned_by: 'Qwen',
+ input_modalities: ['text'],
+ output_modalities: ['text'],
+ context_length: 262144,
+ max_output_length: 235929,
+ supported_features: ['tools', 'json_mode']
+ },
+ { id: 'Qwen/Qwen-Image', input_modalities: ['text'], output_modalities: ['image'] },
+ { id: 'BAAI/bge-m3', input_modalities: ['text'], output_modalities: ['embeddings'] },
+ {
+ id: 'openai/whisper-large-v3-turbo',
+ input_modalities: ['audio'],
+ output_modalities: ['text']
+ },
+ { id: 'missing-input-modalities', output_modalities: ['text'] },
+ { id: 'missing-modalities' }
+ ]
+ })
+ )
+ global.fetch = fetchMock
+ const provider = new AiSdkProvider(defaults, createProviderSettings())
+
+ expect(await provider.fetchModels({ suppressErrors: false })).toEqual([
+ {
+ id: 'Qwen/Qwen3-Next-80B-A3B-Instruct',
+ name: 'Qwen/Qwen3-Next-80B-A3B-Instruct',
+ group: 'default',
+ providerId: 'runcrate',
+ isCustom: false,
+ type: 'chat',
+ contextLength: 262144,
+ maxTokens: 235929,
+ functionCall: true,
+ reasoning: false,
+ vision: false,
+ ownedBy: 'Qwen'
+ }
+ ])
+ expect(fetchMock).toHaveBeenCalledWith(
+ 'https://api.runcrate.ai/v1/models',
+ expect.objectContaining({ method: 'GET' })
+ )
+ })
+
it('routes OpenCode Go chat completions models through OpenAI-compatible runtime', async () => {
const provider = new AiSdkProvider(
createProvider({
diff --git a/test/main/provider/defaultProviders.test.ts b/test/main/provider/defaultProviders.test.ts
index 25cce89c1b..e0542b587b 100644
--- a/test/main/provider/defaultProviders.test.ts
+++ b/test/main/provider/defaultProviders.test.ts
@@ -134,6 +134,23 @@ describe('DEFAULT_PROVIDERS', () => {
)
})
+ it('includes Runcrate as a disabled built-in OpenAI-compatible provider', () => {
+ expect(DEFAULT_PROVIDERS).toContainEqual(
+ expect.objectContaining({
+ id: 'runcrate',
+ name: 'Runcrate',
+ apiType: 'openai-completions',
+ baseUrl: 'https://api.runcrate.ai/v1',
+ enable: false,
+ websites: expect.objectContaining({
+ apiKey: 'https://www.runcrate.ai/dashboard/api-keys?utm_source=deepchat',
+ docs: 'https://www.runcrate.ai/docs?utm_source=deepchat',
+ defaultBaseUrl: 'https://api.runcrate.ai/v1'
+ })
+ })
+ )
+ })
+
it('includes AI Stupid Level as a disabled built-in OpenAI-compatible provider', () => {
expect(DEFAULT_PROVIDERS).toContainEqual(
expect.objectContaining({