feat: add NEAR AI provider integration (#1594)

* feat: add NEAR AI provider integration

- Create vendor, brand, and model descriptors for NEAR AI (22 models)
- Add NEAR AI to route metadata, client, provider auto-detect, and profiles
- Update compatibility tests and ProviderManager test PRESET_ORDER
- Add README and docs entries for NEAR AI provider
- Update .env.example with NEAR AI configuration

* fix: address CodeRabbit review comments

- Fix .env.example: change 'Option N' to 'Option 11' in quick reference
- Narrow isNearaiModelName to use explicit NearAI model prefixes instead of broad includes('/')
- Add NEARAI_API_KEY propagation in strictEnv startup path

* fix: align NEAR AI validation host matching with wildcard subdomain routing

- Add *.completions.near.ai to matchBaseUrlHosts in vendor descriptor
- Add matchHostnameAgainstRouteHosts helper with wildcard (*.) prefix support
- Use helper in both resolveRouteIdFromBaseUrl and getRuntimeValidationTarget
- Add regression test for qwen35-122b.completions.near.ai TEE endpoint
- Add NEARAI_API_KEY to test env cleanup list

* fix: align Near AI integration with env-only provider best practices

- Replace loose .includes('near.ai') with isNearaiBaseUrl() in providerProfiles.ts
  for exact hostname validation (all 4 instances)
- Add NEARAI_API_KEY to copiedOpenAIKeyProvider detection in providerFlag.ts
- Add case 'nearai' to applyProviderFlag switch with dedicated key precedence
- Add 'nearai' to PREFERRED_PROVIDER_ORDER
- Add useNearaiEnvOnlyProvider to OpenAI shim condition in client.ts
- Remove OPENAI_API_KEY exclusion from hasNearaiEnvOnlyProviderIntent (dedicated
  key wins over stale generic key, consistent with xAI pattern)
- Update detection priority comment in providerAutoDetect.ts to include
  MIMO_API_KEY, XAI_API_KEY, and NEARAI_API_KEY

* fix: add exact completions.near.ai host to isNearaiBaseUrl

* fix: add higher-precedence provider key exclusions to hasNearaiEnvOnlyProviderIntent

* fix: add OPENAI_API_KEY and MINIMAX_API_KEY exclusions to hasNearaiEnvOnlyProviderIntent

* fix(near-ai): don't let stale OPENAI_API_KEY suppress Near AI routing

---------

Co-authored-by: Gravirei <gravirei@users.noreply.github.com>
This commit is contained in:
Gravirei
2026-06-11 12:09:25 +08:00
committed by GitHub
co-authored by Gravirei
parent d00b1050f5
commit eacc7d8fac
18 changed files with 748 additions and 16 deletions
+19
View File
@@ -30,6 +30,10 @@
# OPENAI_MODEL=gpt-4o
# OPENAI_BASE_URL=https://api.openai.com/v1 (optional)
#
# Option 11 — NEAR AI (unified gateway: Claude, GPT, Gemini + TEE models):
# NEARAI_API_KEY=your_key_here
# OPENAI_MODEL=anthropic/claude-sonnet-4-6 (optional, default)
#
# Option 3 — Google Gemini:
# CLAUDE_CODE_USE_GEMINI=1
# GEMINI_API_KEY=your-gemini-key-here
@@ -287,6 +291,21 @@ ANTHROPIC_API_KEY=sk-ant-your-key-here
# ANTHROPIC_MODEL=MiniMax-M2.7
# -----------------------------------------------------------------------------
# Option 11: NEAR AI
# -----------------------------------------------------------------------------
# NEAR AI is a unified OpenAI-compatible gateway to Claude, GPT, Gemini,
# and TEE-hosted open models — all under one API key.
# Get your API key from https://cloud.near.ai/dashboard/organizations
#
# NEARAI_API_KEY=your-nearai-key-here
# OPENAI_BASE_URL=https://cloud-api.near.ai/v1 (optional, default)
# OPENAI_MODEL=anthropic/claude-sonnet-4-6 (optional, default)
#
# For direct TEE completions (lower latency + verifiable privacy):
# OPENAI_BASE_URL=https://qwen35-122b.completions.near.ai/v1
# =============================================================================
# OPTIONAL TUNING
# =============================================================================
+1
View File
@@ -171,6 +171,7 @@ Advanced and source-build guides:
| OpenCode Zen | `/provider` or env vars | Pay-as-you-go AI gateway (43 models); uses `OPENCODE_API_KEY` via `https://opencode.ai/zen/v1`; shared key with OpenCode Go |
| OpenCode Go | `/provider` or env vars | $10/mo subscription for open models (13 models); uses `OPENCODE_API_KEY` via `https://opencode.ai/zen/go/v1`; shared key with OpenCode Zen |
| Xiaomi MiMo | `/provider` or env vars | OpenAI-compatible API at `https://mimo.mi.com`; uses `MIMO_API_KEY` and defaults to `mimo-v2.5-pro` |
| NEAR AI | `/provider` or env vars | Unified gateway (Claude, GPT, Gemini + TEE open models); uses `NEARAI_API_KEY` at `https://cloud-api.near.ai/v1` |
| Ollama | `/provider` or env vars | Local inference with no API key |
| Atomic Chat | `/provider`, env vars, or `bun run dev:atomic-chat` | Local Model Provider; auto-detects loaded models |
| Bedrock / Vertex / Foundry | env vars | Anthropic-family cloud routes; Vertex is for Claude on Vertex AI, not arbitrary Model Garden models |
+25
View File
@@ -239,6 +239,31 @@ export OPENAI_MODEL=mimo-v2.5-pro
The `/provider` Xiaomi MiMo preset uses the same endpoint and stores the key as `MIMO_API_KEY`. `OPENAI_API_KEY` also works as a compatibility fallback, but `MIMO_API_KEY` keeps the profile tied to the MiMo route.
### NEAR AI
```bash
export CLAUDE_CODE_USE_OPENAI=1
export NEARAI_API_KEY=...
export OPENAI_BASE_URL=https://cloud-api.near.ai/v1
export OPENAI_MODEL=anthropic/claude-sonnet-4-6
openclaude
```
NEAR AI is a unified OpenAI-compatible gateway that proxies Anthropic, OpenAI,
and Google models alongside TEE-hosted open models (GLM 5.1, Qwen3.5, Kimi K2.6).
All models are accessible from a single endpoint with one API key.
Get your key from https://cloud.near.ai/dashboard/organizations.
Model IDs use `provider/model-name` format (e.g. `anthropic/claude-opus-4-7`,
`openai/gpt-5.5`, `google/gemini-3.5-flash`, `zai-org/GLM-5.1-FP8`).
For direct TEE completions (lower latency, verifiable privacy):
```bash
export OPENAI_BASE_URL=https://qwen35-122b.completions.near.ai/v1
```
### Mistral
```bash
+1
View File
@@ -129,6 +129,7 @@ const PRESET_ORDER = [
'Mistral AI',
'Moonshot AI - API',
'Moonshot AI - Kimi Code',
'NEAR AI',
'NVIDIA NIM',
'OpenAI',
'OpenCode Go',
+38
View File
@@ -0,0 +1,38 @@
import { defineBrand } from '../define.js'
export default defineBrand({
id: 'nearai',
label: 'NEAR AI',
canonicalVendorId: 'nearai',
defaultCapabilities: {
supportsVision: true,
supportsStreaming: true,
supportsFunctionCalling: true,
supportsJsonMode: true,
supportsPreciseTokenCount: false,
},
modelIds: [
'anthropic/claude-opus-4-7',
'anthropic/claude-opus-4-6',
'anthropic/claude-sonnet-4-6',
'anthropic/claude-sonnet-4-5',
'anthropic/claude-haiku-4-5',
'openai/gpt-5.5',
'openai/gpt-5.4',
'openai/gpt-5',
'openai/gpt-4.1',
'openai/gpt-4.1-mini',
'openai/o3',
'openai/o4-mini',
'openai/gpt-oss-120b',
'google/gemini-3.5-flash',
'google/gemini-2.5-pro',
'google/gemini-2.5-flash',
'google/gemma-4-31B-it',
'zai-org/GLM-5.1-FP8',
'Qwen/Qwen3.5-122B-A10B',
'Qwen/Qwen3-30B-A3B-Instruct-2507',
'moonshotai/kimi-k2.6',
'qwen/qwen3.7-max',
],
})
+1
View File
@@ -42,6 +42,7 @@ const EXPECTED_PRESETS = [
'bankr',
'atomic-chat',
'gitlawb-opengateway',
'nearai',
'opencode',
'opencode-go',
] as const satisfies readonly ProviderPreset[]
@@ -9,6 +9,7 @@ import vendorDeepseek from '../vendors/deepseek.js'
import vendorGemini from '../vendors/gemini.js'
import vendorMinimax from '../vendors/minimax.js'
import vendorMoonshot from '../vendors/moonshot.js'
import vendorNearai from '../vendors/nearai.js'
import vendorOpenai from '../vendors/openai.js'
import vendorVenice from '../vendors/venice.js'
import vendorXai from '../vendors/xai.js'
@@ -43,6 +44,7 @@ import brandKimi from '../brands/kimi.js'
import brandLlama from '../brands/llama.js'
import brandMinimax from '../brands/minimax.js'
import brandMistral from '../brands/mistral.js'
import brandNearai from '../brands/nearai.js'
import brandNemotron from '../brands/nemotron.js'
import brandOpenaiCompatibleAlias from '../brands/openai-compatible-alias.js'
import brandQwen from '../brands/qwen.js'
@@ -57,6 +59,7 @@ import modelKimi from '../models/kimi.js'
import modelLlama from '../models/llama.js'
import modelMinimax from '../models/minimax.js'
import modelMistral from '../models/mistral.js'
import modelNearai from '../models/nearai.js'
import modelNemotron from '../models/nemotron.js'
import modelOpenaiCompatibleAlias from '../models/openai-compatible-alias.js'
import modelOpencode from '../models/opencode.js'
@@ -64,11 +67,11 @@ import modelQwen from '../models/qwen.js'
import modelXai from '../models/xai.js'
import modelXiaomiMimo from '../models/xiaomi-mimo.js'
export const VENDOR_DESCRIPTORS = [vendorAnthropic, vendorAtlasCloud, vendorBankr, vendorDeepseek, vendorGemini, vendorMinimax, vendorMoonshot, vendorOpenai, vendorVenice, vendorXai, vendorXiaomiMimo, vendorZai] as const satisfies readonly VendorDescriptor[]
export const VENDOR_DESCRIPTORS = [vendorAnthropic, vendorAtlasCloud, vendorBankr, vendorDeepseek, vendorGemini, vendorMinimax, vendorMoonshot, vendorNearai, vendorOpenai, vendorVenice, vendorXai, vendorXiaomiMimo, vendorZai] as const satisfies readonly VendorDescriptor[]
export const GATEWAY_DESCRIPTORS = [gatewayAtomicChat, gatewayAzureOpenai, gatewayBedrock, gatewayCustom, gatewayDashscopeCn, gatewayDashscopeIntl, gatewayGithub, gatewayGitlawbOpengateway, gatewayGroq, gatewayHicap, gatewayKimiCode, gatewayLmstudio, gatewayMistral, gatewayNvidiaNim, gatewayOllama, gatewayOpencodeGo, gatewayOpencode, gatewayOpenrouter, gatewayTogether, gatewayVertex] as const satisfies readonly GatewayDescriptor[]
export const ANTHROPIC_PROXY_DESCRIPTORS = [] as const satisfies readonly AnthropicProxyDescriptor[]
export const BRAND_DESCRIPTORS = [brandClaude, brandDeepseek, brandGemini, brandGlm, brandGpt, brandKimi, brandLlama, brandMinimax, brandMistral, brandNemotron, brandOpenaiCompatibleAlias, brandQwen, brandXai, brandXiaomiMimo] as const satisfies readonly BrandDescriptor[]
export const MODEL_DESCRIPTOR_GROUPS = [modelClaude, modelDeepseek, modelGemini, modelGlm, modelGpt, modelKimi, modelLlama, modelMinimax, modelMistral, modelNemotron, modelOpenaiCompatibleAlias, modelOpencode, modelQwen, modelXai, modelXiaomiMimo] as const satisfies readonly (readonly ModelDescriptor[])[]
export const BRAND_DESCRIPTORS = [brandClaude, brandDeepseek, brandGemini, brandGlm, brandGpt, brandKimi, brandLlama, brandMinimax, brandMistral, brandNearai, brandNemotron, brandOpenaiCompatibleAlias, brandQwen, brandXai, brandXiaomiMimo] as const satisfies readonly BrandDescriptor[]
export const MODEL_DESCRIPTOR_GROUPS = [modelClaude, modelDeepseek, modelGemini, modelGlm, modelGpt, modelKimi, modelLlama, modelMinimax, modelMistral, modelNearai, modelNemotron, modelOpenaiCompatibleAlias, modelOpencode, modelQwen, modelXai, modelXiaomiMimo] as const satisfies readonly (readonly ModelDescriptor[])[]
export const MODEL_DESCRIPTORS = MODEL_DESCRIPTOR_GROUPS.flat() satisfies readonly ModelDescriptor[]
export const PROVIDER_PRESET_MANIFEST = [
@@ -297,6 +300,19 @@ export const PROVIDER_PRESET_MANIFEST = [
"KIMI_API_KEY"
]
},
{
"preset": "nearai",
"routeKind": "vendor",
"routeId": "nearai",
"vendorId": "nearai",
"description": "NEAR AI unified gateway (Claude, GPT, Gemini + TEE models)",
"apiKeyEnvVars": [
"NEARAI_API_KEY"
],
"modelEnvVars": [
"OPENAI_MODEL"
]
},
{
"preset": "nvidia-nim",
"routeKind": "gateway",
@@ -470,6 +486,7 @@ export const ORDERED_PROVIDER_PRESETS = [
"mistral",
"moonshotai",
"kimi-code",
"nearai",
"nvidia-nim",
"openai",
"opencode-go",
+253
View File
@@ -0,0 +1,253 @@
import { defineModel } from '../define.js'
export default [
// ── Anthropic (proxied) ──
defineModel({
id: 'anthropic/claude-opus-4-7',
defaultModel: 'anthropic/claude-opus-4-7',
label: 'Claude Opus 4.7',
brandId: 'nearai',
vendorId: 'nearai',
classification: ['chat', 'coding'],
capabilities: { supportsVision: false, supportsStreaming: true, supportsFunctionCalling: true, supportsJsonMode: true, supportsReasoning: false, supportsPreciseTokenCount: false },
contextWindow: 1000000,
maxOutputTokens: 65536,
}),
defineModel({
id: 'anthropic/claude-opus-4-6',
defaultModel: 'anthropic/claude-opus-4-6',
label: 'Claude Opus 4.6',
brandId: 'nearai',
vendorId: 'nearai',
classification: ['chat', 'coding'],
capabilities: { supportsVision: false, supportsStreaming: true, supportsFunctionCalling: true, supportsJsonMode: true, supportsReasoning: false, supportsPreciseTokenCount: false },
contextWindow: 200000,
maxOutputTokens: 65536,
}),
defineModel({
id: 'anthropic/claude-sonnet-4-6',
defaultModel: 'anthropic/claude-sonnet-4-6',
label: 'Claude Sonnet 4.6',
brandId: 'nearai',
vendorId: 'nearai',
classification: ['chat', 'coding'],
capabilities: { supportsVision: false, supportsStreaming: true, supportsFunctionCalling: true, supportsJsonMode: true, supportsReasoning: false, supportsPreciseTokenCount: false },
contextWindow: 1000000,
maxOutputTokens: 65536,
}),
defineModel({
id: 'anthropic/claude-sonnet-4-5',
defaultModel: 'anthropic/claude-sonnet-4-5',
label: 'Claude Sonnet 4.5',
brandId: 'nearai',
vendorId: 'nearai',
classification: ['chat', 'coding'],
capabilities: { supportsVision: false, supportsStreaming: true, supportsFunctionCalling: true, supportsJsonMode: true, supportsReasoning: false, supportsPreciseTokenCount: false },
contextWindow: 200000,
maxOutputTokens: 65536,
}),
defineModel({
id: 'anthropic/claude-haiku-4-5',
defaultModel: 'anthropic/claude-haiku-4-5',
label: 'Claude Haiku 4.5',
brandId: 'nearai',
vendorId: 'nearai',
classification: ['chat', 'coding'],
capabilities: { supportsVision: false, supportsStreaming: true, supportsFunctionCalling: true, supportsJsonMode: true, supportsReasoning: false, supportsPreciseTokenCount: false },
contextWindow: 200000,
maxOutputTokens: 65536,
}),
// ── OpenAI (proxied) ──
defineModel({
id: 'openai/gpt-5.5',
defaultModel: 'openai/gpt-5.5',
label: 'GPT-5.5',
brandId: 'nearai',
vendorId: 'nearai',
classification: ['chat', 'coding'],
capabilities: { supportsVision: false, supportsStreaming: true, supportsFunctionCalling: true, supportsJsonMode: true, supportsReasoning: false, supportsPreciseTokenCount: false },
contextWindow: 1050000,
maxOutputTokens: 65536,
}),
defineModel({
id: 'openai/gpt-5.4',
defaultModel: 'openai/gpt-5.4',
label: 'GPT-5.4',
brandId: 'nearai',
vendorId: 'nearai',
classification: ['chat', 'coding'],
capabilities: { supportsVision: false, supportsStreaming: true, supportsFunctionCalling: true, supportsJsonMode: true, supportsReasoning: false, supportsPreciseTokenCount: false },
contextWindow: 1050000,
maxOutputTokens: 65536,
}),
defineModel({
id: 'openai/gpt-5',
defaultModel: 'openai/gpt-5',
label: 'GPT-5',
brandId: 'nearai',
vendorId: 'nearai',
classification: ['chat', 'coding'],
capabilities: { supportsVision: false, supportsStreaming: true, supportsFunctionCalling: true, supportsJsonMode: true, supportsReasoning: false, supportsPreciseTokenCount: false },
contextWindow: 400000,
maxOutputTokens: 65536,
}),
defineModel({
id: 'openai/gpt-4.1',
defaultModel: 'openai/gpt-4.1',
label: 'GPT-4.1',
brandId: 'nearai',
vendorId: 'nearai',
classification: ['chat', 'coding'],
capabilities: { supportsVision: false, supportsStreaming: true, supportsFunctionCalling: true, supportsJsonMode: true, supportsReasoning: false, supportsPreciseTokenCount: false },
contextWindow: 1000000,
maxOutputTokens: 65536,
}),
defineModel({
id: 'openai/gpt-4.1-mini',
defaultModel: 'openai/gpt-4.1-mini',
label: 'GPT-4.1 Mini',
brandId: 'nearai',
vendorId: 'nearai',
classification: ['chat', 'coding'],
capabilities: { supportsVision: false, supportsStreaming: true, supportsFunctionCalling: true, supportsJsonMode: true, supportsReasoning: false, supportsPreciseTokenCount: false },
contextWindow: 1000000,
maxOutputTokens: 65536,
}),
defineModel({
id: 'openai/o3',
defaultModel: 'openai/o3',
label: 'o3',
brandId: 'nearai',
vendorId: 'nearai',
classification: ['chat', 'reasoning'],
capabilities: { supportsVision: false, supportsStreaming: true, supportsFunctionCalling: true, supportsJsonMode: true, supportsReasoning: true, supportsPreciseTokenCount: false },
contextWindow: 200000,
maxOutputTokens: 65536,
}),
defineModel({
id: 'openai/o4-mini',
defaultModel: 'openai/o4-mini',
label: 'o4-mini',
brandId: 'nearai',
vendorId: 'nearai',
classification: ['chat', 'reasoning'],
capabilities: { supportsVision: false, supportsStreaming: true, supportsFunctionCalling: true, supportsJsonMode: true, supportsReasoning: true, supportsPreciseTokenCount: false },
contextWindow: 200000,
maxOutputTokens: 65536,
}),
defineModel({
id: 'openai/gpt-oss-120b',
defaultModel: 'openai/gpt-oss-120b',
label: 'GPT-OSS 120B (TEE)',
brandId: 'nearai',
vendorId: 'nearai',
classification: ['chat'],
capabilities: { supportsVision: false, supportsStreaming: true, supportsFunctionCalling: true, supportsJsonMode: true, supportsReasoning: false, supportsPreciseTokenCount: false },
contextWindow: 131072,
maxOutputTokens: 32768,
}),
// ── Google (proxied) ──
defineModel({
id: 'google/gemini-3.5-flash',
defaultModel: 'google/gemini-3.5-flash',
label: 'Gemini 3.5 Flash',
brandId: 'nearai',
vendorId: 'nearai',
classification: ['chat'],
capabilities: { supportsVision: true, supportsStreaming: true, supportsFunctionCalling: true, supportsJsonMode: true, supportsReasoning: false, supportsPreciseTokenCount: false },
contextWindow: 1000000,
maxOutputTokens: 65536,
}),
defineModel({
id: 'google/gemini-2.5-pro',
defaultModel: 'google/gemini-2.5-pro',
label: 'Gemini 2.5 Pro',
brandId: 'nearai',
vendorId: 'nearai',
classification: ['chat'],
capabilities: { supportsVision: true, supportsStreaming: true, supportsFunctionCalling: true, supportsJsonMode: true, supportsReasoning: false, supportsPreciseTokenCount: false },
contextWindow: 1000000,
maxOutputTokens: 65536,
}),
defineModel({
id: 'google/gemini-2.5-flash',
defaultModel: 'google/gemini-2.5-flash',
label: 'Gemini 2.5 Flash',
brandId: 'nearai',
vendorId: 'nearai',
classification: ['chat'],
capabilities: { supportsVision: true, supportsStreaming: true, supportsFunctionCalling: true, supportsJsonMode: true, supportsReasoning: false, supportsPreciseTokenCount: false },
contextWindow: 1000000,
maxOutputTokens: 65536,
}),
defineModel({
id: 'google/gemma-4-31B-it',
defaultModel: 'google/gemma-4-31B-it',
label: 'Gemma 4 31B (TEE)',
brandId: 'nearai',
vendorId: 'nearai',
classification: ['chat'],
capabilities: { supportsVision: false, supportsStreaming: true, supportsFunctionCalling: true, supportsJsonMode: true, supportsReasoning: false, supportsPreciseTokenCount: false },
contextWindow: 262144,
maxOutputTokens: 32768,
}),
// ── TEE-hosted open models ──
defineModel({
id: 'zai-org/GLM-5.1-FP8',
defaultModel: 'zai-org/GLM-5.1-FP8',
label: 'GLM 5.1 (TEE)',
brandId: 'nearai',
vendorId: 'nearai',
classification: ['chat'],
capabilities: { supportsVision: false, supportsStreaming: true, supportsFunctionCalling: true, supportsJsonMode: true, supportsReasoning: false, supportsPreciseTokenCount: false },
contextWindow: 203000,
maxOutputTokens: 32768,
}),
defineModel({
id: 'Qwen/Qwen3.5-122B-A10B',
defaultModel: 'Qwen/Qwen3.5-122B-A10B',
label: 'Qwen3.5 122B (TEE)',
brandId: 'nearai',
vendorId: 'nearai',
classification: ['chat', 'coding'],
capabilities: { supportsVision: false, supportsStreaming: true, supportsFunctionCalling: true, supportsJsonMode: true, supportsReasoning: false, supportsPreciseTokenCount: false },
contextWindow: 131072,
maxOutputTokens: 32768,
}),
defineModel({
id: 'Qwen/Qwen3-30B-A3B-Instruct-2507',
defaultModel: 'Qwen/Qwen3-30B-A3B-Instruct-2507',
label: 'Qwen3 30B Instruct (TEE)',
brandId: 'nearai',
vendorId: 'nearai',
classification: ['chat'],
capabilities: { supportsVision: false, supportsStreaming: true, supportsFunctionCalling: true, supportsJsonMode: true, supportsReasoning: false, supportsPreciseTokenCount: false },
contextWindow: 262144,
maxOutputTokens: 32768,
}),
defineModel({
id: 'moonshotai/kimi-k2.6',
defaultModel: 'moonshotai/kimi-k2.6',
label: 'Kimi K2.6',
brandId: 'nearai',
vendorId: 'nearai',
classification: ['chat', 'coding'],
capabilities: { supportsVision: false, supportsStreaming: true, supportsFunctionCalling: true, supportsJsonMode: true, supportsReasoning: false, supportsPreciseTokenCount: false },
contextWindow: 262144,
maxOutputTokens: 32768,
}),
defineModel({
id: 'qwen/qwen3.7-max',
defaultModel: 'qwen/qwen3.7-max',
label: 'Qwen3.7 Max',
brandId: 'nearai',
vendorId: 'nearai',
classification: ['chat', 'coding'],
capabilities: { supportsVision: false, supportsStreaming: true, supportsFunctionCalling: true, supportsJsonMode: true, supportsReasoning: false, supportsPreciseTokenCount: false },
contextWindow: 1000000,
maxOutputTokens: 65536,
}),
]
+29
View File
@@ -227,3 +227,32 @@ test('resolveActiveRouteIdFromEnv does not infer MiniMax with OpenAI credentials
}),
).toBe('anthropic')
})
test('resolveActiveRouteIdFromEnv infers Near AI with NEARAI_API_KEY and stale OPENAI_API_KEY', () => {
expect(
resolveActiveRouteIdFromEnv({
NEARAI_API_KEY: 'nearai-key',
OPENAI_API_KEY: 'stale-openai-key',
}),
).toBe('nearai')
})
test('resolveActiveRouteIdFromEnv does not infer Near AI when OPENAI_BASE_URL points elsewhere', () => {
expect(
resolveActiveRouteIdFromEnv({
NEARAI_API_KEY: 'nearai-key',
OPENAI_API_KEY: 'openai-key',
OPENAI_BASE_URL: 'https://api.openai.com/v1',
}),
).toBe('anthropic')
})
test('resolveActiveRouteIdFromEnv does not infer Near AI with explicit provider flag', () => {
expect(
resolveActiveRouteIdFromEnv({
NEARAI_API_KEY: 'nearai-key',
OPENAI_API_KEY: 'openai-key',
CLAUDE_CODE_USE_GEMINI: '1',
}),
).toBe('gemini')
})
+64 -2
View File
@@ -41,6 +41,20 @@ function getValidationRoutingHosts(
return routing?.matchBaseUrlHosts ?? []
}
export function matchHostnameAgainstRouteHosts(
hostname: string,
routeHosts: string[],
): boolean {
return routeHosts.some(host => {
const lowerHost = host.toLowerCase()
if (lowerHost.startsWith('*.')) {
const suffix = lowerHost.slice(1)
return hostname.endsWith(suffix)
}
return hostname === lowerHost
})
}
function normalizeComparableBaseUrl(
baseUrl?: string,
): string | null {
@@ -288,6 +302,36 @@ export function isVeniceBaseUrl(value: string | undefined): boolean {
return false
}
}
export function isNearaiBaseUrl(value: string | undefined): boolean {
const trimmed = value?.trim()
if (!trimmed) {
return false
}
try {
const hostname = new URL(trimmed).hostname.toLowerCase()
return hostname === 'cloud-api.near.ai' || hostname === 'completions.near.ai' || hostname.endsWith('.completions.near.ai')
} catch {
return false
}
}
export function getNearaiBaseUrlOverride(
processEnv: NodeJS.ProcessEnv = process.env,
): string | undefined {
const openAIBaseUrl = processEnv.OPENAI_BASE_URL?.trim()
if (isNearaiBaseUrl(openAIBaseUrl)) {
return openAIBaseUrl
}
const openAIApiBase = processEnv.OPENAI_API_BASE?.trim()
if (isNearaiBaseUrl(openAIApiBase)) {
return openAIApiBase
}
return undefined
}
export function getMiniMaxBaseUrlOverride(
processEnv: NodeJS.ProcessEnv = process.env,
): string | undefined {
@@ -425,9 +469,23 @@ export function hasXiaomiMimoEnvOnlyProviderIntent(
)
}
export function hasNearaiEnvOnlyProviderIntent(
processEnv: NodeJS.ProcessEnv = process.env,
): boolean {
return (
hasNonEmptyEnvValue(processEnv.NEARAI_API_KEY) &&
!hasNonEmptyEnvValue(processEnv.XAI_API_KEY) &&
!hasNonEmptyEnvValue(processEnv.MINIMAX_API_KEY) &&
!hasNonEmptyEnvValue(processEnv.VENICE_API_KEY) &&
!hasNonEmptyEnvValue(processEnv.MIMO_API_KEY) &&
!hasConflictingOpenAIBaseUrlForRoute(processEnv, isNearaiBaseUrl) &&
hasNoExplicitNonOpenAICompatibleProvider(processEnv)
)
}
export function resolveEnvOnlyProviderRouteId(
processEnv: NodeJS.ProcessEnv = process.env,
): 'xai' | 'minimax' | 'venice' | 'xiaomi-mimo' | null {
): 'xai' | 'minimax' | 'venice' | 'xiaomi-mimo' | 'nearai' | null {
if (
hasMiniMaxRouteIntent(processEnv) &&
hasMiniMaxEnvOnlyProviderIntent(processEnv)
@@ -451,6 +509,10 @@ export function resolveEnvOnlyProviderRouteId(
return 'xiaomi-mimo'
}
if (hasNearaiEnvOnlyProviderIntent(processEnv)) {
return 'nearai'
}
return null
}
@@ -620,7 +682,7 @@ export function resolveRouteIdFromBaseUrl(
if (normalizedHost) {
for (const route of routes) {
if (getValidationRoutingHosts(route).includes(normalizedHost)) {
if (matchHostnameAgainstRouteHosts(normalizedHost, getValidationRoutingHosts(route))) {
return route.id
}
}
+174
View File
@@ -0,0 +1,174 @@
import { defineVendor } from '../define.js'
export default defineVendor({
id: 'nearai',
label: 'NEAR AI',
classification: 'openai-compatible',
defaultBaseUrl: 'https://cloud-api.near.ai/v1',
defaultModel: 'anthropic/claude-sonnet-4-6',
requiredEnvVars: ['NEARAI_API_KEY'],
setup: {
requiresAuth: true,
authMode: 'api-key',
credentialEnvVars: ['NEARAI_API_KEY'],
},
transportConfig: {
kind: 'openai-compatible',
},
preset: {
id: 'nearai',
description: 'NEAR AI unified gateway (Claude, GPT, Gemini + TEE models)',
apiKeyEnvVars: ['NEARAI_API_KEY'],
modelEnvVars: ['OPENAI_MODEL'],
},
validation: {
kind: 'credential-env',
routing: {
matchDefaultBaseUrl: true,
matchBaseUrlHosts: ['cloud-api.near.ai', 'completions.near.ai', '*.completions.near.ai'],
},
credentialEnvVars: ['NEARAI_API_KEY'],
missingCredentialMessage: 'NEARAI_API_KEY is required.',
},
catalog: {
source: 'static',
models: [
// ── Anthropic (proxied) ──
{
id: 'anthropic/claude-opus-4-7',
apiName: 'anthropic/claude-opus-4-7',
label: 'Claude Opus 4.7',
modelDescriptorId: 'anthropic/claude-opus-4-7',
},
{
id: 'anthropic/claude-opus-4-6',
apiName: 'anthropic/claude-opus-4-6',
label: 'Claude Opus 4.6',
modelDescriptorId: 'anthropic/claude-opus-4-6',
},
{
id: 'anthropic/claude-sonnet-4-6',
apiName: 'anthropic/claude-sonnet-4-6',
label: 'Claude Sonnet 4.6',
modelDescriptorId: 'anthropic/claude-sonnet-4-6',
},
{
id: 'anthropic/claude-sonnet-4-5',
apiName: 'anthropic/claude-sonnet-4-5',
label: 'Claude Sonnet 4.5',
modelDescriptorId: 'anthropic/claude-sonnet-4-5',
},
{
id: 'anthropic/claude-haiku-4-5',
apiName: 'anthropic/claude-haiku-4-5',
label: 'Claude Haiku 4.5',
modelDescriptorId: 'anthropic/claude-haiku-4-5',
},
// ── OpenAI (proxied) ──
{
id: 'openai/gpt-5.5',
apiName: 'openai/gpt-5.5',
label: 'GPT-5.5',
modelDescriptorId: 'openai/gpt-5.5',
},
{
id: 'openai/gpt-5.4',
apiName: 'openai/gpt-5.4',
label: 'GPT-5.4',
modelDescriptorId: 'openai/gpt-5.4',
},
{
id: 'openai/gpt-5',
apiName: 'openai/gpt-5',
label: 'GPT-5',
modelDescriptorId: 'openai/gpt-5',
},
{
id: 'openai/gpt-4.1',
apiName: 'openai/gpt-4.1',
label: 'GPT-4.1',
modelDescriptorId: 'openai/gpt-4.1',
},
{
id: 'openai/gpt-4.1-mini',
apiName: 'openai/gpt-4.1-mini',
label: 'GPT-4.1 Mini',
modelDescriptorId: 'openai/gpt-4.1-mini',
},
{
id: 'openai/o3',
apiName: 'openai/o3',
label: 'o3',
modelDescriptorId: 'openai/o3',
},
{
id: 'openai/o4-mini',
apiName: 'openai/o4-mini',
label: 'o4-mini',
modelDescriptorId: 'openai/o4-mini',
},
{
id: 'openai/gpt-oss-120b',
apiName: 'openai/gpt-oss-120b',
label: 'GPT-OSS 120B (TEE)',
modelDescriptorId: 'openai/gpt-oss-120b',
},
// ── Google (proxied) ──
{
id: 'google/gemini-3.5-flash',
apiName: 'google/gemini-3.5-flash',
label: 'Gemini 3.5 Flash',
modelDescriptorId: 'google/gemini-3.5-flash',
},
{
id: 'google/gemini-2.5-pro',
apiName: 'google/gemini-2.5-pro',
label: 'Gemini 2.5 Pro',
modelDescriptorId: 'google/gemini-2.5-pro',
},
{
id: 'google/gemini-2.5-flash',
apiName: 'google/gemini-2.5-flash',
label: 'Gemini 2.5 Flash',
modelDescriptorId: 'google/gemini-2.5-flash',
},
{
id: 'google/gemma-4-31B-it',
apiName: 'google/gemma-4-31B-it',
label: 'Gemma 4 31B (TEE)',
modelDescriptorId: 'google/gemma-4-31B-it',
},
// ── TEE-hosted open models ──
{
id: 'zai-org/GLM-5.1-FP8',
apiName: 'zai-org/GLM-5.1-FP8',
label: 'GLM 5.1 (TEE)',
modelDescriptorId: 'zai-org/GLM-5.1-FP8',
},
{
id: 'Qwen/Qwen3.5-122B-A10B',
apiName: 'Qwen/Qwen3.5-122B-A10B',
label: 'Qwen3.5 122B (TEE)',
modelDescriptorId: 'Qwen/Qwen3.5-122B-A10B',
},
{
id: 'Qwen/Qwen3-30B-A3B-Instruct-2507',
apiName: 'Qwen/Qwen3-30B-A3B-Instruct-2507',
label: 'Qwen3 30B Instruct (TEE)',
modelDescriptorId: 'Qwen/Qwen3-30B-A3B-Instruct-2507',
},
{
id: 'moonshotai/kimi-k2.6',
apiName: 'moonshotai/kimi-k2.6',
label: 'Kimi K2.6',
modelDescriptorId: 'moonshotai/kimi-k2.6',
},
{
id: 'qwen/qwen3.7-max',
apiName: 'qwen/qwen3.7-max',
label: 'Qwen3.7 Max',
modelDescriptorId: 'qwen/qwen3.7-max',
},
],
},
})
+42
View File
@@ -36,6 +36,7 @@ import {
} from '../../utils/envUtils.js'
import {
getMiniMaxBaseUrlOverride,
getNearaiBaseUrlOverride,
getRouteDefaultBaseUrl,
getRouteDefaultModel,
getXaiBaseUrlOverride,
@@ -236,6 +237,41 @@ function applyXaiEnvOnlyDefaults(): void {
delete process.env.OPENAI_AUTH_HEADER_VALUE
}
const NEARAI_MODEL_PREFIXES = [
'anthropic/',
'openai/',
'google/',
'zai-org/',
'qwen/',
'moonshotai/',
]
function isNearaiModelName(value: string | undefined): boolean {
const normalized = value?.trim().toLowerCase()
if (!normalized) return false
return NEARAI_MODEL_PREFIXES.some(prefix => normalized.startsWith(prefix))
}
function applyNearaiEnvOnlyDefaults(): void {
const baseUrlOverride = getNearaiBaseUrlOverride()
const hasNearaiBaseOverride = baseUrlOverride !== undefined
const modelOverride = process.env.OPENAI_MODEL?.trim() || undefined
process.env.CLAUDE_CODE_USE_OPENAI = '1'
process.env.OPENAI_BASE_URL =
baseUrlOverride ?? getRouteDefaultBaseUrl('nearai')
process.env.OPENAI_MODEL =
(hasNearaiBaseOverride || isNearaiModelName(modelOverride)
? modelOverride
: undefined) ??
getRouteDefaultModel('nearai')
process.env.OPENAI_API_KEY = process.env.NEARAI_API_KEY
delete process.env.OPENAI_API_FORMAT
delete process.env.OPENAI_AUTH_HEADER
delete process.env.OPENAI_AUTH_SCHEME
delete process.env.OPENAI_AUTH_HEADER_VALUE
}
export async function getAnthropicClient({
apiKey,
maxRetries,
@@ -298,6 +334,8 @@ export async function getAnthropicClient({
envOnlyProviderRouteId === 'xiaomi-mimo' && !useMiniMaxEnvOnlyProvider
const useXaiEnvOnlyProvider =
envOnlyProviderRouteId === 'xai' && !useMiniMaxEnvOnlyProvider
const useNearaiEnvOnlyProvider =
envOnlyProviderRouteId === 'nearai' && !useMiniMaxEnvOnlyProvider
if (useMiniMaxEnvOnlyProvider) {
applyMiniMaxEnvOnlyDefaults(model)
}
@@ -307,6 +345,9 @@ export async function getAnthropicClient({
if (useXaiEnvOnlyProvider) {
applyXaiEnvOnlyDefaults()
}
if (useNearaiEnvOnlyProvider) {
applyNearaiEnvOnlyDefaults()
}
const shouldUseFirstPartyAuth =
shouldUseFirstPartyAnthropicAuth(providerOverride)
@@ -383,6 +424,7 @@ export async function getAnthropicClient({
if (
useXiaomiMimoEnvOnlyProvider ||
useXaiEnvOnlyProvider ||
useNearaiEnvOnlyProvider ||
isEnvTruthy(process.env.CLAUDE_CODE_USE_OPENAI) ||
isEnvTruthy(process.env.CLAUDE_CODE_USE_GITHUB) ||
isEnvTruthy(process.env.CLAUDE_CODE_USE_GEMINI) ||
+13 -3
View File
@@ -16,9 +16,11 @@
* 5. GEMINI_API_KEY or GOOGLE_API_KEY
* 6. MISTRAL_API_KEY
* 7. MINIMAX_API_KEY
* 8. XAI_API_KEY
* 9. Local Ollama reachable (default localhost:11434)
* 10. Local LM Studio reachable (default localhost:1234)
* 8. MIMO_API_KEY
* 9. XAI_API_KEY
* 10. NEARAI_API_KEY
* 11. Local Ollama reachable (default localhost:11434)
* 12. Local LM Studio reachable (default localhost:1234)
*
* Local-service probes are parallelized and cheap (short timeout, no
* request body). Env scans are synchronous and run first so we don't make
@@ -43,6 +45,7 @@ export type DetectedProviderKind =
| 'minimax'
| 'xiaomi-mimo'
| 'xai'
| 'nearai'
| 'ollama'
| 'lm-studio'
| 'gitlawb-opengateway'
@@ -171,6 +174,13 @@ export function detectProviderFromEnv(
return { kind: 'xai', source: 'XAI_API_KEY set' }
}
if (envHasNonEmpty(env, 'NEARAI_API_KEY')) {
return {
kind: 'nearai',
source: 'NEARAI_API_KEY set',
}
}
return null
}
+21 -3
View File
@@ -41,6 +41,7 @@ const PREFERRED_PROVIDER_ORDER = [
'minimax',
'venice',
'atlas-cloud',
'nearai',
] as const
function buildValidProviders(): string[] {
@@ -285,10 +286,13 @@ export function applyProviderFlag(
: process.env.OPENAI_API_KEY !== undefined &&
process.env.OPENAI_API_KEY === process.env.MINIMAX_API_KEY
? 'minimax'
: process.env.OPENAI_API_KEY !== undefined &&
process.env.OPENAI_API_KEY === process.env.ATLAS_CLOUD_API_KEY
? 'atlas-cloud'
: process.env.OPENAI_API_KEY !== undefined &&
process.env.OPENAI_API_KEY === process.env.ATLAS_CLOUD_API_KEY
? 'atlas-cloud'
: process.env.OPENAI_API_KEY !== undefined &&
process.env.OPENAI_API_KEY === process.env.NEARAI_API_KEY
? 'nearai'
: process.env.OPENAI_API_KEY !== undefined &&
opengatewayApiKey !== undefined &&
opengatewayApiKey.length > 0 &&
process.env.OPENAI_API_KEY === opengatewayApiKey
@@ -408,6 +412,20 @@ export function applyProviderFlag(
}
break
case 'nearai':
process.env.CLAUDE_CODE_USE_OPENAI = '1'
applyOpenAIBaseUrlDefault(provider, defaultBaseUrl)
if (defaultModel) {
process.env.OPENAI_MODEL ??= defaultModel
}
if (model) process.env.OPENAI_MODEL = model
if (process.env.NEARAI_API_KEY) {
process.env.OPENAI_API_KEY = process.env.NEARAI_API_KEY
} else {
delete process.env.OPENAI_API_KEY
}
break
default:
process.env.CLAUDE_CODE_USE_OPENAI = '1'
applyOpenAIBaseUrlDefault(provider, defaultBaseUrl)
+6 -1
View File
@@ -98,6 +98,7 @@ const PROFILE_ENV_KEYS = [
'VENICE_API_KEY',
'MIMO_API_KEY',
'ATLAS_CLOUD_API_KEY',
'NEARAI_API_KEY',
'OPENCODE_API_KEY',
DEFAULT_STARTUP_PROVIDER_ENV_VAR,
] as const
@@ -125,6 +126,7 @@ const SECRET_ENV_KEYS = [
'VENICE_API_KEY',
'MIMO_API_KEY',
'ATLAS_CLOUD_API_KEY',
'NEARAI_API_KEY',
'OPENCODE_API_KEY',
] as const
@@ -185,6 +187,7 @@ export type ProfileEnv = {
VENICE_API_KEY?: string
MIMO_API_KEY?: string
ATLAS_CLOUD_API_KEY?: string
NEARAI_API_KEY?: string
OPENCODE_API_KEY?: string
}
@@ -209,7 +212,8 @@ type SecretValueSource = Partial<
| 'XAI_API_KEY'
| 'VENICE_API_KEY'
| 'MIMO_API_KEY'
| 'ATLAS_CLOUD_API_KEY',
| 'ATLAS_CLOUD_API_KEY'
| 'NEARAI_API_KEY',
string | undefined
>
>
@@ -1629,6 +1633,7 @@ export async function buildLaunchEnv(options: {
// unauthenticated.
for (const dedicatedKey of [
'ATLAS_CLOUD_API_KEY',
'NEARAI_API_KEY',
'MIMO_API_KEY',
'VENICE_API_KEY',
] as const) {
+19 -1
View File
@@ -44,7 +44,7 @@ import {
type ResolvedProfileRoute,
type ProviderPreset,
} from '../integrations/index.js'
import { resolveEnvOnlyProviderRouteId } from '../integrations/routeMetadata.js'
import { isNearaiBaseUrl, resolveEnvOnlyProviderRouteId } from '../integrations/routeMetadata.js'
import { logForDebugging } from './debug.js'
import {
sanitizeProfileCustomHeaders,
@@ -553,6 +553,10 @@ function isProcessEnvAlignedWithProfile(
(profile.baseUrl?.toLowerCase().includes('atlascloud')
? !includeApiKey ||
sameOptionalEnvValue(processEnv.ATLAS_CLOUD_API_KEY, profile.apiKey)
: true) &&
(isNearaiBaseUrl(profile.baseUrl)
? !includeApiKey ||
sameOptionalEnvValue(processEnv.NEARAI_API_KEY, profile.apiKey)
: true)
)
}
@@ -691,6 +695,9 @@ export function applyProviderProfileToProcessEnv(profile: ProviderProfile): void
if (route.routeId === 'atlas-cloud' || profile.baseUrl.toLowerCase().includes('atlascloud')) {
openAIProfileEnv.ATLAS_CLOUD_API_KEY = profile.apiKey
}
if (route.routeId === 'nearai' || isNearaiBaseUrl(profile.baseUrl)) {
openAIProfileEnv.NEARAI_API_KEY = profile.apiKey
}
}
if (route.gatewayId === 'nvidia-nim') {
openAIProfileEnv.NVIDIA_NIM = '1'
@@ -936,6 +943,9 @@ function buildOpenAICompatibleStartupEnv(
if (activeProfile.baseUrl?.toLowerCase().includes('atlascloud')) {
strictEnv.ATLAS_CLOUD_API_KEY = activeProfile.apiKey
}
if (isNearaiBaseUrl(activeProfile.baseUrl)) {
strictEnv.NEARAI_API_KEY = activeProfile.apiKey
}
return applySupportedProfileCustomHeaders(activeProfile, strictEnv)
}
}
@@ -976,6 +986,9 @@ function buildOpenAICompatibleStartupEnv(
if (activeProfile.baseUrl?.toLowerCase().includes('atlascloud')) {
env.ATLAS_CLOUD_API_KEY = activeProfile.apiKey
}
if (isNearaiBaseUrl(activeProfile.baseUrl)) {
env.NEARAI_API_KEY = activeProfile.apiKey
}
} else {
delete env.OPENAI_API_KEY
}
@@ -1129,6 +1142,11 @@ function buildStartupProfileFromActiveProfile(
: null
}
if (route.vendorId === 'nearai') {
const env = buildOpenAICompatibleStartupEnv(activeProfile)
return env ? { profile: 'openai', env } : null
}
// xAI OAuth profile (provider=xai with no API key). Tag the startup
// file with profile='xai' + XAI_CREDENTIAL_SOURCE=oauth so:
// 1. validation accepts it at startup (no spurious
+19
View File
@@ -39,6 +39,7 @@ const ENV_KEYS = [
'GOOGLE_APPLICATION_CREDENTIALS',
'XAI_API_KEY',
'XAI_CREDENTIAL_SOURCE',
'NEARAI_API_KEY',
] as const
const originalEnv: Record<string, string | undefined> = {}
@@ -318,6 +319,24 @@ test('xiaomi mimo validation accepts MIMO_API_KEY without OPENAI_API_KEY', async
await expect(getProviderValidationError(process.env)).resolves.toBeNull()
})
test('nearai validation accepts NEARAI_API_KEY for cloud-api base URL', async () => {
process.env.CLAUDE_CODE_USE_OPENAI = '1'
process.env.OPENAI_BASE_URL = 'https://cloud-api.near.ai/v1'
process.env.NEARAI_API_KEY = 'nearai-live-key'
delete process.env.OPENAI_API_KEY
await expect(getProviderValidationError(process.env)).resolves.toBeNull()
})
test('nearai validation accepts NEARAI_API_KEY for wildcard TEE completions endpoint', async () => {
process.env.CLAUDE_CODE_USE_OPENAI = '1'
process.env.OPENAI_BASE_URL = 'https://qwen35-122b.completions.near.ai/v1'
process.env.NEARAI_API_KEY = 'nearai-tee-key'
delete process.env.OPENAI_API_KEY
await expect(getProviderValidationError(process.env)).resolves.toBeNull()
})
test('opengateway validation fails without OPENGATEWAY_API_KEY or OPENAI_API_KEY', async () => {
process.env.CLAUDE_CODE_USE_OPENAI = '1'
process.env.OPENAI_BASE_URL = 'https://opengateway.gitlawb.com/v1'
+3 -3
View File
@@ -15,6 +15,7 @@ import {
getRouteCredentialValue,
getRouteDescriptor,
getRouteDefaultModel,
matchHostnameAgainstRouteHosts,
resolveActiveRouteIdFromEnv,
resolveRouteIdFromBaseUrl,
} from '../integrations/routeMetadata.js'
@@ -234,9 +235,8 @@ function getRuntimeValidationTarget(
}
return (
routing.matchBaseUrlHosts?.some(
host => requestHost === host.toLowerCase(),
) ?? false
routing.matchBaseUrlHosts &&
matchHostnameAgainstRouteHosts(requestHost, routing.matchBaseUrlHosts)
)
})