Repository navigation
feat(integrations): add ApiSmart OpenAI-compatible gateway provider #2109
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all 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 a926e33
fix(apismart): protect dedicated credentials
jatmn 324d9b5
fix(apismart): enforce route credential boundaries
jatmn 513b884
fix(apismart): honor explicit competing route models
jatmn be559ab
fix(apismart): validate credentials and default profiles
jatmn 5e233c1
test(apismart): isolate env-only client tests
jatmn 3194fe3
fix(apismart): align routing and validation contracts
jatmn f8cd975
fix(apismart): centralize route capability contracts
jatmn f4f6f86
Revert "fix(apismart): centralize route capability contracts"
jatmn e32ed66
fix(apismart): reject template credential placeholders at the shared …
jatmn 37df782
fix(apismart): reject null base-URL and placeholder profile keys
jatmn fdcf668
fix(apismart): match AIMLAPI env-only intent and proxy credential wit…
jatmn bf11b2a
fix(apismart): restore AIMLAPI credential and canonical URL parity
jatmn 54708bf
fix: simplify apismart gateway integration
jatmn 5aea211
fix: reject credential placeholders consistently
jatmn 1fab2d9
test: remove obsolete apismart exception coverage
jatmn 2a330b4
test: remove stale apismart model fixture
jatmn 5bd52e8
fix(apismart): restore dedicated provider contract
jatmn 0137006
fix(apismart): enforce credential boundaries
jatmn 75ce4fe
fix(apismart): clear inherited auth headers
jatmn 22bd7bc
test(apismart): strengthen credential boundaries
jatmn File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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() | ||
| }) | ||
| }) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 }, | ||
| }) |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.