mirror of
https://github.com/Gitlawb/openclaude.git
synced 2026-08-24 10:14:19 -05:00
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:
@@ -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
|
||||
# =============================================================================
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -129,6 +129,7 @@ const PRESET_ORDER = [
|
||||
'Mistral AI',
|
||||
'Moonshot AI - API',
|
||||
'Moonshot AI - Kimi Code',
|
||||
'NEAR AI',
|
||||
'NVIDIA NIM',
|
||||
'OpenAI',
|
||||
'OpenCode Go',
|
||||
|
||||
@@ -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',
|
||||
],
|
||||
})
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
]
|
||||
@@ -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')
|
||||
})
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+174
@@ -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',
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
@@ -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) ||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user