Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
cb94011
feat(gateway): add Concentrate AI provider with dynamic model discovery
jatmn Aug 18, 2026
16faa99
fix(concentrate): prompt for Concentrate API key in /provider instead…
jatmn Aug 18, 2026
3e154c7
docs(concentrate): remove standalone setup guide to match other gateways
jatmn Aug 18, 2026
a843b37
fix(concentrate): dedicated-credential-only routing, env-only identit…
jatmn Aug 18, 2026
1d5c66f
fix(concentrate): protect credentials on noncanonical urls
jatmn Aug 18, 2026
90a1fb8
fix(concentrate): drop legacy keys on retargeted profiles
jatmn Aug 18, 2026
fae0c21
fix(concentrate): remove legacy generic keys on proxies
jatmn Aug 18, 2026
6992d4b
fix(concentrate): reject noncanonical credential routes
jatmn Aug 18, 2026
1ee6d09
fix(concentrate): validate env-only credentials
jatmn Aug 18, 2026
1eb5aba
fix(concentrate): align env-only route validation
jatmn Aug 18, 2026
e88cbfa
fix(concentrate): clear headers before client setup
jatmn Aug 18, 2026
d30d8a9
test(validation): isolate Concentrate model env
jatmn Aug 18, 2026
a52cf2b
fix(concentrate): honor provider precedence and model defaults
jatmn Aug 18, 2026
fd93e09
test(concentrate): cover model resolution precedence
jatmn Aug 18, 2026
922b285
fix(concentrate): preserve legacy model fallback
jatmn Aug 18, 2026
aa2bb4a
fix(concentrate): normalize early model selection
jatmn Aug 18, 2026
3d6a601
fix(concentrate): validate and select rejected models safely
jatmn Aug 18, 2026
48243f3
fix(concentrate): reset stale route state
jatmn Aug 18, 2026
75aa0f2
fix(concentrate): preserve proxy profile capabilities
jatmn Aug 19, 2026
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
13 changes: 13 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,19 @@ ANTHROPIC_API_KEY=sk-ant-your-key-here
#
# Usage limits are displayed via /usage command.

# -----------------------------------------------------------------------------
# Option 15: Concentrate AI
# -----------------------------------------------------------------------------
# Concentrate AI is a unified OpenAI-compatible gateway to 150+ models.
# Get your API key from https://concentrate.ai
#
# CLAUDE_CODE_USE_OPENAI=1
# CONCENTRATE_API_KEY=sk-cn-your-key-here
# CONCENTRATE_BASE_URL=https://api.concentrate.ai/v1 (optional, default)
# CONCENTRATE_MODEL=deepseek-v4-flash (optional, default)
#
# Or configure interactively with /provider

# =============================================================================
# OPTIONAL TUNING
# =============================================================================
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,7 @@ Advanced and source-build guides:
| OpenAI-compatible | `/provider` or env vars | Works with OpenAI, OpenRouter, DeepSeek, Groq, Mistral, LM Studio, and other compatible `/v1` servers |
| Z.AI GLM Coding Plan | `/provider` or OpenAI-compatible env vars | Uses `OPENAI_API_KEY` at `https://api.z.ai/api/coding/paas/v4` and defaults to `glm-5.2` |
| AI/ML API | `/provider` or `AIMLAPI_API_KEY` ([setup guide](docs/aimlapi-setup.md)) | Uses `https://api.aimlapi.com/v1`, auto-detects the OpenAI-compatible route from `AIMLAPI_API_KEY`, sends OpenClaude attribution headers, and discovers chat-capable models from the public `/models` catalog |
| Concentrate | `/provider` or `CONCENTRATE_API_KEY` | Unified OpenAI-compatible gateway at `https://api.concentrate.ai/v1`; defaults to `deepseek-v4-flash` and auto-discovers the chat model catalog |
| ApiSmart | `/provider` or `APISMART_API_KEY` | Uses `https://gw.apismart.ai/v1`, defaults to `DEEPSEEK_V4_FLASH`, and supports optional `APISMART_MODEL` plus authenticated model discovery |
| Hicap | `/provider` or OpenAI-compatible env vars | Uses `api-key` auth, discovers models from unauthenticated `/models`, and supports Responses mode for `gpt-` models |
| Fireworks AI | `/provider` or env vars | First-class provider with 276 curated models (DeepSeek, Qwen, Llama, Gemma, and more); uses `FIREWORKS_API_KEY` |
Expand Down
1 change: 1 addition & 0 deletions src/components/ProviderManager.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ const PRESET_ORDER = [
'Bankr',
'ClinePass',
'Cloudflare Workers AI',
'Concentrate',
'DeepSeek',
'Codex OAuth',
'xAI OAuth (Grok)',
Expand Down
79 changes: 79 additions & 0 deletions src/integrations/gateways/concentrate.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { describe, expect, test } from 'bun:test'

import concentrate from './concentrate.js'

const mapModel = concentrate.catalog?.discovery?.mapModel

describe('concentrate gateway', () => {
test('uses dynamic discovery and supports fallback auth', () => {
expect(concentrate.id).toBe('concentrate')
expect(concentrate.label).toBe('Concentrate')
expect(concentrate.category).toBe('aggregating')
expect(concentrate.defaultBaseUrl).toBe('https://api.concentrate.ai/v1')
expect(concentrate.defaultModel).toBe('deepseek-v4-flash')
expect(concentrate.supportsModelRouting).toBe(true)

expect(concentrate.setup.requiresAuth).toBe(true)
expect(concentrate.setup.authMode).toBe('api-key')
expect(concentrate.setup.credentialEnvVars).toEqual(['CONCENTRATE_API_KEY'])
expect(concentrate.setup.dedicatedCredentialsOnly).toBeUndefined()

expect(concentrate.catalog?.source).toBe('dynamic')
expect(concentrate.catalog?.discovery?.kind).toBe('openai-compatible')
expect(concentrate.catalog?.discovery?.requiresAuth).toBe(false)

expect(concentrate.validation?.kind).toBe('credential-env')
expect(
(concentrate.validation as { credentialEnvVars: string[] }).credentialEnvVars,
).toEqual([
'CONCENTRATE_API_KEY',
'OPENAI_API_KEYS',
'OPENAI_API_KEY',
])

expect(mapModel).toBeDefined()
})

test('mapModel keeps chat models and drops non-chat ids', () => {
if (!mapModel) throw new Error('mapModel missing')

expect(
mapModel({
id: 'deepseek-v4-flash',
display_name: 'DeepSeek V4 Flash',
owned_by: 'deepseek',
max_input_tokens: 1_048_576,
max_tokens: 393_216,
}),
).toEqual({
id: 'deepseek-v4-flash',
apiName: 'deepseek-v4-flash',
label: 'DeepSeek V4 Flash',
contextWindow: 1_048_576,
maxOutputTokens: 393_216,
})

expect(
mapModel({
id: 'claude-sonnet-5',
display_name: 'Claude Sonnet 5',
owned_by: 'anthropic',
max_input_tokens: 200_000,
max_tokens: 64_000,
}),
).toEqual({
id: 'claude-sonnet-5',
apiName: 'claude-sonnet-5',
label: 'Claude Sonnet 5',
contextWindow: 200_000,
maxOutputTokens: 64_000,
})

expect(mapModel({ id: 'redact-v1' })).toBeNull()
expect(mapModel({ id: 'gpt-oss-safeguard-120b' })).toBeNull()
expect(mapModel({ id: 'text-embedding-3-small' })).toBeNull()
expect(mapModel({ id: 'whisper-1' })).toBeNull()
expect(mapModel({})).toBeNull()
expect(mapModel(null)).toBeNull()
Comment thread
jatmn marked this conversation as resolved.
})
})
111 changes: 111 additions & 0 deletions src/integrations/gateways/concentrate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { defineGateway } from '../define.js'

const NON_CHAT_MODEL_PATTERN =
/redact|safeguard|embed|embedding|whisper|tts|dall-e|rerank|moderation|image-generation|video-generation|audio-generation/i

function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null
}

function getTrimmedString(
record: Record<string, unknown>,
key: string,
): string | undefined {
const value = record[key]
return typeof value === 'string' ? value.trim() : undefined
}

function getPositiveInteger(value: unknown): number | undefined {
if (
typeof value === 'number' &&
Number.isFinite(value) &&
Number.isInteger(value) &&
value > 0
) {
return value
}
return undefined
}

function mapConcentrateModel(raw: unknown) {
if (!isRecord(raw)) {
return null
}

const id = getTrimmedString(raw, 'id')
if (!id || NON_CHAT_MODEL_PATTERN.test(id)) {
return null
}

const displayName = getTrimmedString(raw, 'display_name')
const label = displayName || id
const contextWindow = getPositiveInteger(raw.max_input_tokens)
const maxOutputTokens = getPositiveInteger(raw.max_tokens)

return {
id,
apiName: id,
label,
...(contextWindow !== undefined ? { contextWindow } : {}),
...(maxOutputTokens !== undefined ? { maxOutputTokens } : {}),
}
}

export default defineGateway({
id: 'concentrate',
label: 'Concentrate',
category: 'aggregating',
defaultBaseUrl: 'https://api.concentrate.ai/v1',
defaultModel: 'deepseek-v4-flash',
supportsModelRouting: true,
setup: {
requiresAuth: true,
authMode: 'api-key',
credentialEnvVars: ['CONCENTRATE_API_KEY'],
},
startup: {
probeReadiness: 'openai-compatible-models',
},
transportConfig: {
kind: 'openai-compatible',
openaiShim: {
supportsApiFormatSelection: false,
supportsAuthHeaders: false,
maxTokensField: 'max_tokens',
},
},
preset: {
id: 'concentrate',
description: 'Concentrate AI — 150+ models via OpenAI-compatible API',
vendorId: 'openai',
apiKeyEnvVars: ['CONCENTRATE_API_KEY'],
baseUrlEnvVars: ['CONCENTRATE_BASE_URL'],
modelEnvVars: ['CONCENTRATE_MODEL', 'OPENAI_MODEL'],
},
validation: {
kind: 'credential-env',
routing: {
matchDefaultBaseUrl: true,
matchBaseUrlHosts: ['api.concentrate.ai'],
},
credentialEnvVars: [
'CONCENTRATE_API_KEY',
'OPENAI_API_KEYS',
'OPENAI_API_KEY',
],
missingCredentialMessage:
'Concentrate auth is required. Set CONCENTRATE_API_KEY or OPENAI_API_KEY.',
},
catalog: {
source: 'dynamic',
discovery: {
kind: 'openai-compatible',
requiresAuth: false,
mapModel: mapConcentrateModel,
},
discoveryCacheTtl: '1d',
discoveryRefreshMode: 'startup',
allowManualRefresh: true,
},
usage: { supported: false },
})
3 changes: 2 additions & 1 deletion src/integrations/generated/integrationArtifacts.generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import gatewayAzureOpenai from '../gateways/azure-openai.js'
import gatewayBedrock from '../gateways/bedrock.js'
import gatewayClinepass from '../gateways/clinepass.js'
import gatewayCloudflare from '../gateways/cloudflare.js'
import gatewayConcentrate from '../gateways/concentrate.js'
import gatewayCustom from '../gateways/custom.js'
import gatewayDashscopeCn from '../gateways/dashscope-cn.js'
import gatewayDashscopeIntl from '../gateways/dashscope-intl.js'
Expand Down Expand Up @@ -90,7 +91,7 @@ import modelXai from '../models/xai.js'
import modelXiaomiMimo from '../models/xiaomi-mimo.js'

export const VENDOR_DESCRIPTORS = [vendorAnthropic, vendorBankr, vendorDeepseek, vendorFireworks, vendorGemini, vendorLongcat, vendorMinimax, vendorMoonshot, vendorNearai, vendorOpenai, vendorVenice, vendorXai, vendorXiaomiMimo, vendorZai] as const satisfies readonly VendorDescriptor[]
export const GATEWAY_DESCRIPTORS = [gatewayAimlapi, gatewayApismart, gatewayAtlasCloud, gatewayAtomicChat, gatewayAzureOpenai, gatewayBedrock, gatewayClinepass, gatewayCloudflare, gatewayCustom, gatewayDashscopeCn, gatewayDashscopeIntl, gatewayGithubEnterprise, gatewayGithub, gatewayGitlawbOpengateway, gatewayGroq, gatewayHicap, gatewayKimiCode, gatewayLmstudio, gatewayMistral, gatewayNvidiaNim, gatewayOllama, gatewayOpencodeGo, gatewayOpencode, gatewayOpenrouter, gatewayTogether, gatewayVertex, gatewayXiaomiMimoToken] as const satisfies readonly GatewayDescriptor[]
export const GATEWAY_DESCRIPTORS = [gatewayAimlapi, gatewayApismart, gatewayAtlasCloud, gatewayAtomicChat, gatewayAzureOpenai, gatewayBedrock, gatewayClinepass, gatewayCloudflare, gatewayConcentrate, gatewayCustom, gatewayDashscopeCn, gatewayDashscopeIntl, gatewayGithubEnterprise, gatewayGithub, gatewayGitlawbOpengateway, gatewayGroq, gatewayHicap, gatewayKimiCode, gatewayLmstudio, gatewayMistral, gatewayNvidiaNim, gatewayOllama, gatewayOpencodeGo, gatewayOpencode, gatewayOpenrouter, gatewayTogether, gatewayVertex, gatewayXiaomiMimoToken] as const satisfies readonly GatewayDescriptor[]
export const ANTHROPIC_PROXY_DESCRIPTORS = [anthropicproxyCustom] as const satisfies readonly AnthropicProxyDescriptor[]
export const BRAND_DESCRIPTORS = [brandClaude, brandDeepseek, brandFireworks, brandGemini, brandGlm, brandGpt, brandKimi, brandLing, brandLlama, brandLongcat, brandMacaron, brandMinimax, brandMistral, brandNearai, brandNemotron, brandOpenaiCompatibleAlias, brandQwen, brandTencent, brandXai, brandXiaomiMimo] as const satisfies readonly BrandDescriptor[]
export const MODEL_DESCRIPTOR_GROUPS = [modelClaude, modelDeepseek, modelFireworksMerged, modelGemini, modelGlm, modelGpt, modelKimi, modelLing, modelLlama, modelLongcat, modelMacaron, modelMinimax, modelMistral, modelNearai, modelNemotron, modelOpenaiCompatibleAlias, modelOpencode, modelQwen, modelTencent, modelXai, modelXiaomiMimo] as const satisfies readonly (readonly ModelDescriptor[])[]
Expand Down
19 changes: 19 additions & 0 deletions src/integrations/generated/integrationManifest.generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,24 @@ export const PROVIDER_PRESET_MANIFEST = [
"OPENAI_MODEL"
]
},
{
"preset": "concentrate",
"routeKind": "gateway",
"routeId": "concentrate",
"vendorId": "openai",
"gatewayId": "concentrate",
"description": "Concentrate AI — 150+ models via OpenAI-compatible API",
"apiKeyEnvVars": [
"CONCENTRATE_API_KEY"
],
"baseUrlEnvVars": [
"CONCENTRATE_BASE_URL"
],
"modelEnvVars": [
"CONCENTRATE_MODEL",
"OPENAI_MODEL"
]
},
{
"preset": "deepseek",
"routeKind": "vendor",
Expand Down Expand Up @@ -548,6 +566,7 @@ export const ORDERED_PROVIDER_PRESETS = [
"bankr",
"clinepass",
"cloudflare",
"concentrate",
"deepseek",
"fireworks",
"gemini",
Expand Down
1 change: 1 addition & 0 deletions src/integrations/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ describe('loaded registry validation', () => {
test('gateway defaultModel values are present unless provided outside curated catalog metadata', () => {
const routesWithExternalDefaultModelSources = new Set([
'atomic-chat',
'concentrate',
'custom',
'lmstudio',
'ollama',
Expand Down
Loading
Loading