Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
e915dcb
feat(integrations): add ApiSmart OpenAI-compatible gateway provider
jatmn Aug 10, 2026
a926e33
fix(apismart): protect dedicated credentials
jatmn Aug 10, 2026
324d9b5
fix(apismart): enforce route credential boundaries
jatmn Aug 10, 2026
513b884
fix(apismart): honor explicit competing route models
jatmn Aug 10, 2026
be559ab
fix(apismart): validate credentials and default profiles
jatmn Aug 10, 2026
5e233c1
test(apismart): isolate env-only client tests
jatmn Aug 10, 2026
3194fe3
fix(apismart): align routing and validation contracts
jatmn Aug 10, 2026
f8cd975
fix(apismart): centralize route capability contracts
jatmn Aug 10, 2026
f4f6f86
Revert "fix(apismart): centralize route capability contracts"
jatmn Aug 10, 2026
e32ed66
fix(apismart): reject template credential placeholders at the shared …
jatmn Aug 10, 2026
37df782
fix(apismart): reject null base-URL and placeholder profile keys
jatmn Aug 10, 2026
fdcf668
fix(apismart): match AIMLAPI env-only intent and proxy credential wit…
jatmn Aug 10, 2026
bf11b2a
fix(apismart): restore AIMLAPI credential and canonical URL parity
jatmn Aug 11, 2026
54708bf
fix: simplify apismart gateway integration
jatmn Aug 11, 2026
5aea211
fix: reject credential placeholders consistently
jatmn Aug 11, 2026
1fab2d9
test: remove obsolete apismart exception coverage
jatmn Aug 11, 2026
2a330b4
test: remove stale apismart model fixture
jatmn Aug 11, 2026
5bd52e8
fix(apismart): restore dedicated provider contract
jatmn Aug 11, 2026
0137006
fix(apismart): enforce credential boundaries
jatmn Aug 11, 2026
75ce4fe
fix(apismart): clear inherited auth headers
jatmn Aug 11, 2026
22bd7bc
test(apismart): strengthen credential boundaries
jatmn Aug 11, 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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,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 |
| 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` |
| LongCat | `/provider` or env vars | Meituan LongCat OpenAI-compatible API at `https://api.longcat.chat/openai/v1`; uses `LONGCAT_API_KEY` and defaults to `LongCat-2.0` |
Expand Down
2 changes: 2 additions & 0 deletions scripts/system-check.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ const ENV_KEYS = [
'VENICE_API_KEY',
'MIMO_API_KEY',
'ATLAS_CLOUD_API_KEY',
'APISMART_API_KEY',
'APISMART_MODEL',
'NEARAI_API_KEY',
'FIREWORKS_API_KEY',
'CLINE_API_KEY',
Expand Down
1 change: 1 addition & 0 deletions src/integrations/compatibility.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import type { ProviderPreset } from '../utils/providerProfiles.js'
const EXPECTED_PRESETS = [
'anthropic',
'atlas-cloud',
'apismart',
'aimlapi',
'openai',
'ollama',
Expand Down
44 changes: 44 additions & 0 deletions src/integrations/gateways/apismart.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { describe, expect, test } from 'bun:test'

import apismart from './apismart.js'

const mapModel = apismart.catalog?.discovery?.mapModel

describe('apismart gateway', () => {
test('uses hybrid discovery with dedicated credentials', () => {
expect(apismart.id).toBe('apismart')
expect(apismart.catalog?.source).toBe('hybrid')
expect(apismart.catalog?.discovery?.kind).toBe('openai-compatible')
expect(apismart.catalog?.discovery?.requiresAuth).toBe(true)
expect(apismart.setup.dedicatedCredentialsOnly).toBe(true)
expect(apismart.setup.credentialEnvVars).toEqual(['APISMART_API_KEY'])
expect(apismart.defaultBaseUrl).toBe('https://gw.apismart.ai/v1')
expect(apismart.defaultModel).toBe('DEEPSEEK_V4_FLASH')
expect(mapModel).toBeDefined()
})

test('curated catalog keeps ApiSmart case-sensitive model ids', () => {
const models = apismart.catalog?.models ?? []
expect(models.some(model => model.apiName === 'DEEPSEEK_V4_FLASH')).toBe(
true,
)
expect(models.some(model => model.apiName === 'GLM_5.2')).toBe(true)
expect(models.some(model => model.apiName === 'QWEN_3_7_MAX')).toBe(true)
})

test('mapModel keeps chat models and drops image/video ids', () => {
if (!mapModel) throw new Error('mapModel missing')
expect(mapModel({ id: 'DEEPSEEK_V4_FLASH', owned_by: 'DeepSeek' })).toEqual(
{
id: 'DEEPSEEK_V4_FLASH',
apiName: 'DEEPSEEK_V4_FLASH',
label: 'DEEPSEEK_V4_FLASH (DeepSeek)',
},
)
expect(mapModel({ id: 'seedream-5.0' })).toBeNull()
expect(mapModel({ id: 'seedance-2.0' })).toBeNull()
expect(mapModel({ id: 'happyhorse-1.0' })).toBeNull()
expect(mapModel({})).toBeNull()
expect(mapModel(null)).toBeNull()
})
})
227 changes: 227 additions & 0 deletions src/integrations/gateways/apismart.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
import { defineGateway } from '../define.js'

const NON_CHAT_MODEL_PATTERN =
/(seedream|seedance|happyhorse|image|video|vision-preview)/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 mapApismartModel(raw: unknown) {
if (!isRecord(raw)) {
return null
}

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

const ownedBy =
getTrimmedString(raw, 'owned_by') || getTrimmedString(raw, 'ownedBy')
const label = ownedBy ? `${id} (${ownedBy})` : id

return {
id,
apiName: id,
label,
}
}

// Curated from https://www.apismart.ai/models (PAGE_MODEL_LIST LLM entries).
// Exact Model IDs are case-sensitive per ApiSmart docs.
const curatedModels = [
{
id: 'DEEPSEEK_V4_FLASH',
apiName: 'DEEPSEEK_V4_FLASH',
aliases: ['deepseek-v4-flash'],
modelDescriptorId: 'deepseek-v4-flash',
label: 'DeepSeek V4 Flash',
contextWindow: 1_048_576,
maxOutputTokens: 8_192,
},
{
id: 'DEEPSEEK_V4_PRO',
apiName: 'DEEPSEEK_V4_PRO',
aliases: ['deepseek-v4-pro'],
modelDescriptorId: 'deepseek-v4-pro',
label: 'DeepSeek V4 Pro',
contextWindow: 1_048_576,
maxOutputTokens: 16_384,
},
{
id: 'DEEPSEEK_V3_2',
apiName: 'DEEPSEEK_V3_2',
aliases: ['deepseek-v3.2'],
modelDescriptorId: 'deepseek-ai/deepseek-v3.2',
label: 'DeepSeek V3.2',
contextWindow: 131_072,
maxOutputTokens: 8_192,
},
{
id: 'KIMI_K2_6',
apiName: 'KIMI_K2_6',
aliases: ['kimi-k2.6'],
modelDescriptorId: 'kimi-k2.6',
label: 'Kimi K2.6',
contextWindow: 262_144,
maxOutputTokens: 16_384,
},
{
id: 'KIMI_K2_5',
apiName: 'KIMI_K2_5',
aliases: ['kimi-k2.5'],
modelDescriptorId: 'kimi-k2.5',
label: 'Kimi K2.5',
contextWindow: 262_144,
maxOutputTokens: 16_384,
},
{
id: 'KIMI_K3',
apiName: 'KIMI_K3',
aliases: ['kimi-k3', 'k3'],
modelDescriptorId: 'k3',
label: 'Kimi K3',
contextWindow: 1_075_200,
maxOutputTokens: 8_192,
},
{
id: 'GLM_5.2',
apiName: 'GLM_5.2',
aliases: ['glm-5.2'],
modelDescriptorId: 'glm-5.2',
label: 'GLM 5.2',
contextWindow: 1_048_576,
maxOutputTokens: 16_384,
},
{
id: 'GLM_5_1',
apiName: 'GLM_5_1',
aliases: ['glm-5.1'],
modelDescriptorId: 'glm-5.1',
label: 'GLM 5.1',
contextWindow: 204_800,
maxOutputTokens: 16_384,
},
{
id: 'GLM_5',
apiName: 'GLM_5',
aliases: ['glm-5'],
modelDescriptorId: 'glm-5',
label: 'GLM 5',
contextWindow: 209_920,
maxOutputTokens: 16_384,
},
{
id: 'QWEN_3_7_MAX',
apiName: 'QWEN_3_7_MAX',
aliases: ['qwen3.7-max'],
modelDescriptorId: 'qwen3.7-max',
label: 'Qwen 3.7 Max',
contextWindow: 1_048_576,
maxOutputTokens: 16_384,
},
{
id: 'QWEN_3_6_PLUS',
apiName: 'QWEN_3_6_PLUS',
aliases: ['qwen3.6-plus'],
modelDescriptorId: 'qwen3.6-plus',
label: 'Qwen 3.6 Plus',
contextWindow: 1_048_576,
maxOutputTokens: 16_384,
},
{
id: 'MINIMAX_M3',
apiName: 'MINIMAX_M3',
aliases: ['minimax-m3'],
modelDescriptorId: 'minimax-m3',
label: 'MiniMax M3',
contextWindow: 1_048_576,
maxOutputTokens: 16_384,
},
{
id: 'MINIMAX_M2_5',
apiName: 'MINIMAX_M2_5',
aliases: ['minimax-m2.5'],
modelDescriptorId: 'minimax-m2.5',
label: 'MiniMax M2.5',
contextWindow: 200_704,
maxOutputTokens: 16_384,
},
{
id: 'MINIMAX_M2_5_HIGHSPEED',
apiName: 'MINIMAX_M2_5_HIGHSPEED',
aliases: ['minimax-m2.5-highspeed'],
modelDescriptorId: 'minimax-m2.5-highspeed',
label: 'MiniMax M2.5 HighSpeed',
contextWindow: 204_800,
maxOutputTokens: 16_384,
},
]

export default defineGateway({
id: 'apismart',
label: 'ApiSmart',
category: 'aggregating',
defaultBaseUrl: 'https://gw.apismart.ai/v1',
defaultModel: 'DEEPSEEK_V4_FLASH',
supportsModelRouting: true,
setup: {
requiresAuth: true,
authMode: 'api-key',
credentialEnvVars: ['APISMART_API_KEY'],
dedicatedCredentialsOnly: true,
},
startup: {
probeReadiness: 'openai-compatible-models',
},
transportConfig: {
kind: 'openai-compatible',
openaiShim: {
supportsApiFormatSelection: false,
supportsAuthHeaders: false,
// ApiSmart chat-completions examples use max_tokens.
maxTokensField: 'max_tokens',
},
},
preset: {
id: 'apismart',
description: 'ApiSmart unified OpenAI-compatible gateway',
vendorId: 'openai',
apiKeyEnvVars: ['APISMART_API_KEY'],
modelEnvVars: ['APISMART_MODEL', 'OPENAI_MODEL'],
},
validation: {
kind: 'credential-env',
routing: {
matchDefaultBaseUrl: true,
matchBaseUrlHosts: ['gw.apismart.ai'],
},
credentialEnvVars: ['APISMART_API_KEY'],
missingCredentialMessage:
'ApiSmart auth is required. Set APISMART_API_KEY.',
},
catalog: {
// /v1/models exists and requires a Bearer key; curated LLM ids stay visible
// before discovery and when refresh fails.
source: 'hybrid',
discovery: {
kind: 'openai-compatible',
requiresAuth: true,
mapModel: mapApismartModel,
},
discoveryCacheTtl: '1d',
discoveryRefreshMode: 'background-if-stale',
allowManualRefresh: true,
models: [...curatedModels],
},
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 @@ -20,6 +20,7 @@ import vendorXai from '../vendors/xai.js'
import vendorXiaomiMimo from '../vendors/xiaomi-mimo.js'
import vendorZai from '../vendors/zai.js'
import gatewayAimlapi from '../gateways/aimlapi.js'
import gatewayApismart from '../gateways/apismart.js'
import gatewayAtlasCloud from '../gateways/atlas-cloud.js'
import gatewayAtomicChat from '../gateways/atomic-chat.js'
import gatewayAzureOpenai from '../gateways/azure-openai.js'
Expand Down Expand Up @@ -89,7 +90,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, 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, 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
16 changes: 16 additions & 0 deletions src/integrations/generated/integrationManifest.generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,21 @@ export const PROVIDER_PRESET_MANIFEST = [
"DASHSCOPE_API_KEY"
]
},
{
"preset": "apismart",
"routeKind": "gateway",
"routeId": "apismart",
"vendorId": "openai",
"gatewayId": "apismart",
"description": "ApiSmart unified OpenAI-compatible gateway",
"apiKeyEnvVars": [
"APISMART_API_KEY"
],
"modelEnvVars": [
"APISMART_MODEL",
"OPENAI_MODEL"
]
},
{
"preset": "atlas-cloud",
"routeKind": "gateway",
Expand Down Expand Up @@ -527,6 +542,7 @@ export const ORDERED_PROVIDER_PRESETS = [
"anthropic",
"dashscope-cn",
"dashscope-intl",
"apismart",
"atlas-cloud",
"azure-openai",
"bankr",
Expand Down
Loading