From 9621a9354b7b2223ac8bae01e1b9e129e5d446b2 Mon Sep 17 00:00:00 2001
From: Nirupam Bhowmick <48842933+heydryft@users.noreply.github.com>
Date: Sun, 25 Oct 2026 07:28:58 +0000
Subject: [PATCH 1/4] feat(provider): add Runcrate
---
README.jp.md | 4 +++
README.md | 4 +++
README.zh.md | 4 +++
src/main/provider/defaults.ts | 15 +++++++++
src/main/provider/providerRegistry.ts | 12 +++++++
.../src/assets/llm-icons/runcrate.svg | 1 +
.../src/components/icons/modelIconRegistry.ts | 3 ++
.../provider/basicApiKeyProviders.test.ts | 33 +++++++++++++++++++
test/main/provider/defaultProviders.test.ts | 17 ++++++++++
9 files changed, 93 insertions(+)
create mode 100644 src/renderer/src/assets/llm-icons/runcrate.svg
diff --git a/README.jp.md b/README.jp.md
index 33efe2ee3..4aab4223b 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 689bb8012..1f789e270 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 7e6cb1ed2..6780f2989 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 51b6f586e..d8657bdf6 100644
--- a/src/main/provider/defaults.ts
+++ b/src/main/provider/defaults.ts
@@ -650,6 +650,21 @@ 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&utm_medium=directory&utm_campaign=gpu_pricing',
+ apiKey: 'https://www.runcrate.ai/dashboard/api-keys',
+ docs: 'https://www.runcrate.ai/docs',
+ models: 'https://www.runcrate.ai/inference'
+ }
+ },
{
id: 'aistupidlevel',
name: 'AI Stupid Level',
diff --git a/src/main/provider/providerRegistry.ts b/src/main/provider/providerRegistry.ts
index 561e1d1a2..8174243f9 100644
--- a/src/main/provider/providerRegistry.ts
+++ b/src/main/provider/providerRegistry.ts
@@ -611,6 +611,18 @@ const PROVIDER_ID_REGISTRY = new Map([
credentialStrategy: 'api-key'
})
],
+ [
+ 'runcrate',
+ createDefinition({
+ ...OPENAI_BASE,
+ 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/renderer/src/assets/llm-icons/runcrate.svg b/src/renderer/src/assets/llm-icons/runcrate.svg
new file mode 100644
index 000000000..6d083c98f
--- /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 b62ab381c..80dbe4dad 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 9fba0738a..d1087545e 100644
--- a/test/main/provider/basicApiKeyProviders.test.ts
+++ b/test/main/provider/basicApiKeyProviders.test.ts
@@ -1689,6 +1689,39 @@ 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: 'openai',
+ 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('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 25cce89c1..103cba18d 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',
+ docs: 'https://www.runcrate.ai/docs',
+ 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({
From 60646b22c6acf2d281999d290b6709458f7d10cb Mon Sep 17 00:00:00 2001
From: Nirupam Bhowmick <48842933+heydryft@users.noreply.github.com>
Date: Mon, 26 Oct 2026 08:31:17 +0000
Subject: [PATCH 2/4] feat(provider): list only Runcrate text chat models
Add a runcrate modelSource that keeps text-in, text-out models from the public /v1/models catalog and maps context_length, max_output_length and supported_features. Use utm_source=deepchat on every Runcrate link.
---
README.jp.md | 2 +-
README.md | 2 +-
README.zh.md | 2 +-
src/main/provider/defaults.ts | 9 ++--
src/main/provider/providerRegistry.ts | 2 +
src/main/provider/providers/aiSdkProvider.ts | 49 +++++++++++++++++
.../provider/basicApiKeyProviders.test.ts | 53 ++++++++++++++++++-
test/main/provider/defaultProviders.test.ts | 4 +-
8 files changed, 112 insertions(+), 11 deletions(-)
diff --git a/README.jp.md b/README.jp.md
index 4aab4223b..0d9574a12 100644
--- a/README.jp.md
+++ b/README.jp.md
@@ -462,7 +462,7 @@ DeepChatはメッセージアプリからリモート操作できるため、デ

- Runcrate
+ Runcrate
|
diff --git a/README.md b/README.md
index 1f789e270..cdac50c61 100644
--- a/README.md
+++ b/README.md
@@ -465,7 +465,7 @@ Common commands include `/start`, `/help`, `/pair`, `/new`, `/sessions`, `/use`,

- Runcrate
+ Runcrate
|
diff --git a/README.zh.md b/README.zh.md
index 6780f2989..c0d31e190 100644
--- a/README.zh.md
+++ b/README.zh.md
@@ -459,7 +459,7 @@ DeepChat 可以通过聊天软件远程控制,让你离开桌面后也能继

- Runcrate
+ Runcrate
|
diff --git a/src/main/provider/defaults.ts b/src/main/provider/defaults.ts
index d8657bdf6..58c09115f 100644
--- a/src/main/provider/defaults.ts
+++ b/src/main/provider/defaults.ts
@@ -658,11 +658,10 @@ const PROVIDER_DEFAULTS: LLM_PROVIDER_BASE[] = [
baseUrl: 'https://api.runcrate.ai/v1',
enable: false,
websites: {
- official:
- 'https://www.runcrate.ai/?utm_source=deepchat&utm_medium=directory&utm_campaign=gpu_pricing',
- apiKey: 'https://www.runcrate.ai/dashboard/api-keys',
- docs: 'https://www.runcrate.ai/docs',
- models: 'https://www.runcrate.ai/inference'
+ 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'
}
},
{
diff --git a/src/main/provider/providerRegistry.ts b/src/main/provider/providerRegistry.ts
index 8174243f9..608aefe28 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'
@@ -615,6 +616,7 @@ const PROVIDER_ID_REGISTRY = new Map([
'runcrate',
createDefinition({
...OPENAI_BASE,
+ modelSource: 'runcrate',
checkStrategy: 'generate-text',
credentialStrategy: 'api-key',
checkModelId: 'deepseek-ai/DeepSeek-V4.1-Flash',
diff --git a/src/main/provider/providers/aiSdkProvider.ts b/src/main/provider/providers/aiSdkProvider.ts
index 74eda8628..fb92e34e6 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,53 @@ 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 && !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/test/main/provider/basicApiKeyProviders.test.ts b/test/main/provider/basicApiKeyProviders.test.ts
index d1087545e..01e4dd98d 100644
--- a/test/main/provider/basicApiKeyProviders.test.ts
+++ b/test/main/provider/basicApiKeyProviders.test.ts
@@ -1696,7 +1696,7 @@ describe('basic API-key provider registrations', () => {
// Runcrate's /v1/models answers without a key, so listing models cannot validate one.
expect(resolveAiSdkProviderDefinition(config)).toMatchObject({
runtimeKind: 'openai-compatible',
- modelSource: 'openai',
+ modelSource: 'runcrate',
checkStrategy: 'generate-text',
credentialStrategy: 'api-key',
checkModelId: 'deepseek-ai/DeepSeek-V4.1-Flash'
@@ -1722,6 +1722,57 @@ describe('basic API-key provider registrations', () => {
)
})
+ 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-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 103cba18d..e0542b587 100644
--- a/test/main/provider/defaultProviders.test.ts
+++ b/test/main/provider/defaultProviders.test.ts
@@ -143,8 +143,8 @@ describe('DEFAULT_PROVIDERS', () => {
baseUrl: 'https://api.runcrate.ai/v1',
enable: false,
websites: expect.objectContaining({
- apiKey: 'https://www.runcrate.ai/dashboard/api-keys',
- docs: 'https://www.runcrate.ai/docs',
+ 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'
})
})
From 84854f5ca5c87d7bdcf29f8a4353928b6891b14c Mon Sep 17 00:00:00 2001
From: Nirupam Bhowmick <48842933+heydryft@users.noreply.github.com>
Date: Mon, 26 Oct 2026 08:52:59 +0000
Subject: [PATCH 3/4] fix(provider): require text input for Runcrate chat
models
---
src/main/provider/providers/aiSdkProvider.ts | 2 +-
test/main/provider/basicApiKeyProviders.test.ts | 1 +
2 files changed, 2 insertions(+), 1 deletion(-)
diff --git a/src/main/provider/providers/aiSdkProvider.ts b/src/main/provider/providers/aiSdkProvider.ts
index fb92e34e6..5d48a2a1c 100644
--- a/src/main/provider/providers/aiSdkProvider.ts
+++ b/src/main/provider/providers/aiSdkProvider.ts
@@ -2156,7 +2156,7 @@ export class AiSdkProvider extends BaseLLMProvider {
if (
!modelId ||
!outputModalities?.includes('text') ||
- (inputModalities && !inputModalities.includes('text'))
+ !inputModalities?.includes('text')
) {
continue
}
diff --git a/test/main/provider/basicApiKeyProviders.test.ts b/test/main/provider/basicApiKeyProviders.test.ts
index 01e4dd98d..175615c49 100644
--- a/test/main/provider/basicApiKeyProviders.test.ts
+++ b/test/main/provider/basicApiKeyProviders.test.ts
@@ -1744,6 +1744,7 @@ describe('basic API-key provider registrations', () => {
input_modalities: ['audio'],
output_modalities: ['text']
},
+ { id: 'missing-input-modalities', output_modalities: ['text'] },
{ id: 'missing-modalities' }
]
})
From 27323b6363a884b85762f1f9825840621effa355 Mon Sep 17 00:00:00 2001
From: Nirupam Bhowmick <48842933+heydryft@users.noreply.github.com>
Date: Mon, 26 Oct 2026 15:29:24 +0000
Subject: [PATCH 4/4] style(provider): format Runcrate catalog guard
---
src/main/provider/providers/aiSdkProvider.ts | 6 +-----
1 file changed, 1 insertion(+), 5 deletions(-)
diff --git a/src/main/provider/providers/aiSdkProvider.ts b/src/main/provider/providers/aiSdkProvider.ts
index 5d48a2a1c..ea30e7bae 100644
--- a/src/main/provider/providers/aiSdkProvider.ts
+++ b/src/main/provider/providers/aiSdkProvider.ts
@@ -2153,11 +2153,7 @@ export class AiSdkProvider extends BaseLLMProvider {
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')
- ) {
+ if (!modelId || !outputModalities?.includes('text') || !inputModalities?.includes('text')) {
continue
}
const features = toStrings(model.supported_features)