mirror of
https://github.com/Gitlawb/openclaude.git
synced 2026-08-24 02:34:15 -05:00
feat(providers): add focused LLMTR hybrid gateway (#2150)
* feat: add LLMTR hybrid gateway * feat: support LLMTR_API_KEY * fix: allow LLMTR provider env files * fix: protect LLMTR credential routing * fix: protect LLMTR profile credentials * fix: complete LLMTR env lifecycle * fix: select LLMTR model before client setup * fix: address LLMTR review findings * fix: route LLMTR auxiliary models correctly * fix: complete LLMTR credential boundaries * fix: clear persisted LLMTR startup keys * fix: normalize LLMTR generic credentials * fix: scope LLMTR credential support * fix: align LLMTR credential boundaries * fix: close LLMTR credential boundaries * fix: clear stale LLMTR auth state * fix: clear persisted LLMTR credentials * fix: complete LLMTR setup contracts * fix: close LLMTR lifecycle gaps * fix(providers): close LLMTR credential boundaries * fix(providers): preserve saved LLMTR profile keys
This commit is contained in:
@@ -211,6 +211,13 @@ ANTHROPIC_API_KEY=sk-ant-your-key-here
|
||||
# OPENAI_BASE_URL=https://api.aimlapi.com/v1
|
||||
# OPENAI_MODEL=gpt-4o
|
||||
|
||||
# For LLMTR, prefer its dedicated key. Raw env setup must also set
|
||||
# OPENAI_BASE_URL and OPENAI_MODEL. OPENAI_API_KEY remains supported:
|
||||
# LLMTR_API_KEY=your-llmtr-key-here
|
||||
# OPENAI_API_KEY=your-llmtr-key-here # supported fallback
|
||||
# OPENAI_BASE_URL=https://llmtr.com/v1
|
||||
# OPENAI_MODEL=deepseek/deepseek-v4-flash
|
||||
|
||||
# Use a custom OpenAI-compatible endpoint (optional — defaults to api.openai.com)
|
||||
# OPENAI_BASE_URL=https://api.openai.com/v1
|
||||
# Choose the OpenAI-compatible API surface (optional).
|
||||
|
||||
@@ -309,6 +309,7 @@ Advanced and source-build guides:
|
||||
| Z.AI GLM Coding Plan | `/provider` or OpenAI-compatible env vars | Uses `OPENAI_API_KEY` at `https://api.z.ai/api/coding/paas/v4` and defaults to `glm-5.2` |
|
||||
| AI/ML API | `/provider` or `AIMLAPI_API_KEY` ([setup guide](docs/aimlapi-setup.md)) | Uses `https://api.aimlapi.com/v1`, auto-detects the OpenAI-compatible route from `AIMLAPI_API_KEY`, sends OpenClaude attribution headers, and discovers chat-capable models from the public `/models` catalog |
|
||||
| Concentrate | `/provider` or `CONCENTRATE_API_KEY` | Unified OpenAI-compatible gateway at `https://api.concentrate.ai/v1`; defaults to `deepseek-v4-flash` and auto-discovers the chat model catalog |
|
||||
| LLMTR | `/provider` or OpenAI-compatible env vars | Multi-model gateway at `https://llmtr.com/v1`; `/provider` and `--provider llmtr` default to `deepseek/deepseek-v4-flash`, while raw env setup must set `OPENAI_BASE_URL=https://llmtr.com/v1` and `OPENAI_MODEL`; accepts `LLMTR_API_KEY` or `OPENAI_API_KEY` after the route is selected and discovers tool-capable Chat Completions models from the public catalog |
|
||||
| ApiSmart | `/provider` or `APISMART_API_KEY` | Uses `https://gw.apismart.ai/v1`, defaults to `DEEPSEEK_V4_FLASH`, and supports optional `APISMART_MODEL` plus authenticated model discovery |
|
||||
| Hicap | `/provider` or OpenAI-compatible env vars | Uses `api-key` auth, discovers models from unauthenticated `/models`, and supports Responses mode for `gpt-` models |
|
||||
| Fireworks AI | `/provider` or env vars | First-class provider with 276 curated models (DeepSeek, Qwen, Llama, Gemma, and more); uses `FIREWORKS_API_KEY` |
|
||||
|
||||
@@ -136,6 +136,7 @@ const PRESET_ORDER = [
|
||||
'Anthropic',
|
||||
'Alibaba Coding Plan (China)',
|
||||
'Alibaba Coding Plan',
|
||||
'ApiSmart',
|
||||
'Atlas Cloud',
|
||||
'Azure OpenAI',
|
||||
'Bankr',
|
||||
@@ -149,6 +150,7 @@ const PRESET_ORDER = [
|
||||
'Google AI / Gemini',
|
||||
'Groq',
|
||||
'Hicap',
|
||||
'LLMTR',
|
||||
'LM Studio',
|
||||
'Atomic Chat',
|
||||
'Ollama',
|
||||
@@ -308,6 +310,17 @@ function mockProviderProfilesModule(options?: {
|
||||
}
|
||||
}
|
||||
|
||||
if (preset === 'llmtr') {
|
||||
return {
|
||||
provider: 'llmtr',
|
||||
name: 'LLMTR',
|
||||
baseUrl: 'https://llmtr.com/v1',
|
||||
model: 'deepseek/deepseek-v4-flash',
|
||||
apiKey: '',
|
||||
requiresApiKey: true,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
provider: 'openai',
|
||||
name: 'Mock provider',
|
||||
@@ -1126,6 +1139,130 @@ test('ProviderManager asks for model and API key when adding OpenAI preset', asy
|
||||
}
|
||||
})
|
||||
|
||||
test('ProviderManager adds LLMTR with only model selection and API key', async () => {
|
||||
const addProviderProfile = mock((payload: any) => ({
|
||||
id: 'llmtr_profile',
|
||||
...payload,
|
||||
}))
|
||||
|
||||
mockProviderManagerDependencies(() => undefined, async () => undefined, {
|
||||
addProviderProfile,
|
||||
})
|
||||
|
||||
const nonce = `${Date.now()}-${Math.random()}`
|
||||
const { ProviderManager } = await import(`./ProviderManager.js?ts=${nonce}`)
|
||||
const mounted = await mountProviderManager(ProviderManager)
|
||||
|
||||
try {
|
||||
await waitForFrameOutput(mounted.getOutput, frame =>
|
||||
frame.includes('Provider manager'),
|
||||
)
|
||||
|
||||
mounted.stdin.write('\r')
|
||||
await waitForFrameOutput(mounted.getOutput, frame =>
|
||||
frame.includes('Choose provider preset'),
|
||||
)
|
||||
|
||||
await navigateToPreset(mounted.stdin, 'LLMTR')
|
||||
mounted.stdin.write('\r')
|
||||
const modelOutput = await waitForFrameOutput(mounted.getOutput, frame =>
|
||||
frame.includes('Create provider profile') &&
|
||||
frame.includes('Step 1 of 2: Default model'),
|
||||
)
|
||||
|
||||
expect(modelOutput).toContain('LLMTR')
|
||||
expect(modelOutput).toContain('deepseek/deepseek-v4-flash')
|
||||
expect(modelOutput).not.toContain('Provider name')
|
||||
expect(modelOutput).not.toContain('Base URL')
|
||||
expect(modelOutput).not.toContain('API mode')
|
||||
expect(modelOutput).not.toContain('Custom headers')
|
||||
|
||||
mounted.stdin.write('\r')
|
||||
const keyOutput = await waitForFrameOutput(mounted.getOutput, frame =>
|
||||
frame.includes('Step 2 of 2: API key'),
|
||||
)
|
||||
expect(keyOutput).not.toContain('Provider name')
|
||||
expect(keyOutput).not.toContain('Base URL')
|
||||
expect(keyOutput).not.toContain('API mode')
|
||||
expect(keyOutput).not.toContain('Custom headers')
|
||||
|
||||
mounted.stdin.write('llmtr-test-key')
|
||||
await Bun.sleep(25)
|
||||
mounted.stdin.write('\r')
|
||||
|
||||
await waitForCondition(() => addProviderProfile.mock.calls.length > 0)
|
||||
expect(addProviderProfile).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
provider: 'llmtr',
|
||||
name: 'LLMTR',
|
||||
baseUrl: 'https://llmtr.com/v1',
|
||||
model: 'deepseek/deepseek-v4-flash',
|
||||
apiKey: 'llmtr-test-key',
|
||||
apiFormat: 'chat_completions',
|
||||
}),
|
||||
expect.objectContaining({ makeActive: true }),
|
||||
)
|
||||
} finally {
|
||||
await mounted.dispose()
|
||||
}
|
||||
})
|
||||
|
||||
test('ProviderManager edits query-bearing LLMTR endpoints with generic proxy controls', async () => {
|
||||
const llmtrProxyProfile = {
|
||||
id: 'provider_llmtr_proxy',
|
||||
provider: 'llmtr',
|
||||
name: 'LLMTR query proxy',
|
||||
baseUrl: 'https://llmtr.com/v1?tenant=proxy',
|
||||
model: 'proxy-model',
|
||||
apiKey: undefined,
|
||||
apiFormat: 'responses',
|
||||
authHeader: 'X-Proxy-Key',
|
||||
authScheme: 'raw',
|
||||
authHeaderValue: 'proxy-auth-value',
|
||||
customHeaders: { 'X-Proxy-Trace': 'enabled' },
|
||||
}
|
||||
|
||||
mockProviderManagerDependencies(
|
||||
() => undefined,
|
||||
async () => undefined,
|
||||
{
|
||||
getProviderProfiles: () => [llmtrProxyProfile],
|
||||
getActiveProviderProfile: () => llmtrProxyProfile,
|
||||
},
|
||||
)
|
||||
|
||||
const nonce = `${Date.now()}-${Math.random()}`
|
||||
const { ProviderManager } = await import(`./ProviderManager.js?ts=${nonce}`)
|
||||
const mounted = await mountProviderManager(ProviderManager)
|
||||
|
||||
try {
|
||||
await waitForFrameOutput(mounted.getOutput, frame =>
|
||||
frame.includes('Provider manager') && frame.includes('Edit provider'),
|
||||
)
|
||||
|
||||
mounted.stdin.write('j')
|
||||
await Bun.sleep(25)
|
||||
mounted.stdin.write('j')
|
||||
await Bun.sleep(25)
|
||||
mounted.stdin.write('\r')
|
||||
|
||||
await waitForFrameOutput(mounted.getOutput, frame =>
|
||||
frame.includes('Edit provider') &&
|
||||
frame.includes('LLMTR query proxy') &&
|
||||
!frame.includes('Provider manager'),
|
||||
)
|
||||
|
||||
mounted.stdin.write('\r')
|
||||
const editOutput = await waitForFrameOutput(mounted.getOutput, frame =>
|
||||
frame.includes('Edit provider profile') && frame.includes('Step 1 of 8'),
|
||||
)
|
||||
|
||||
expect(editOutput).toContain('Advanced: this provider supports custom request headers')
|
||||
} finally {
|
||||
await mounted.dispose()
|
||||
}
|
||||
})
|
||||
|
||||
test('ProviderManager saves OpenAI preset GPT-5 models with Responses API', async () => {
|
||||
const addProviderProfile = mock((payload: any) => ({
|
||||
id: 'openai_profile',
|
||||
|
||||
@@ -90,6 +90,7 @@ import {
|
||||
getActiveProviderProfile,
|
||||
getProviderPresetDefaults,
|
||||
getProviderProfiles,
|
||||
resolveProfileCapabilityRouteId,
|
||||
setActiveProviderProfile,
|
||||
type ProviderPreset,
|
||||
type ProviderProfileInput,
|
||||
@@ -368,12 +369,7 @@ function resolveProviderEditorRouteId(
|
||||
provider: ProviderProfile['provider'],
|
||||
baseUrl?: string,
|
||||
): string {
|
||||
const route = resolveProfileRoute(provider).routeId
|
||||
if (route !== 'openai') {
|
||||
return route
|
||||
}
|
||||
|
||||
return resolveRouteIdFromBaseUrl(baseUrl) ?? route
|
||||
return resolveProfileCapabilityRouteId(provider, baseUrl)
|
||||
}
|
||||
|
||||
function routeSupportsResponsesModel(routeId: string, model: string): boolean {
|
||||
@@ -2539,6 +2535,10 @@ export function ProviderManager({ mode, onDone }: Props): React.ReactNode {
|
||||
}
|
||||
|
||||
function renderForm(): React.ReactNode {
|
||||
const editorRouteId = resolveProviderEditorRouteId(
|
||||
draftProvider,
|
||||
draft.baseUrl,
|
||||
)
|
||||
return (
|
||||
<Box flexDirection="column" gap={1}>
|
||||
<Text color="remember" bold>
|
||||
@@ -2547,9 +2547,9 @@ export function ProviderManager({ mode, onDone }: Props): React.ReactNode {
|
||||
<Text dimColor>{displayStep.helpText}</Text>
|
||||
<Text dimColor>
|
||||
Provider type:{' '}
|
||||
{getRouteProviderTypeLabel(resolveProfileRoute(draftProvider).routeId)}
|
||||
{getRouteProviderTypeLabel(editorRouteId)}
|
||||
</Text>
|
||||
{routeSupportsCustomHeaders(resolveProfileRoute(draftProvider).routeId) ? (
|
||||
{routeSupportsCustomHeaders(editorRouteId) ? (
|
||||
<Text dimColor>
|
||||
Advanced: this provider supports custom request headers when you
|
||||
need them.
|
||||
|
||||
@@ -49,9 +49,11 @@ const EXPECTED_PRESETS = [
|
||||
'atomic-chat',
|
||||
'cloudflare',
|
||||
'gitlawb-opengateway',
|
||||
'concentrate',
|
||||
'nearai',
|
||||
'fireworks',
|
||||
'longcat',
|
||||
'llmtr',
|
||||
'opencode',
|
||||
'opencode-go',
|
||||
'clinepass',
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
import { defineCatalog } from '../define.js'
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null
|
||||
}
|
||||
|
||||
function hasStringValue(value: unknown, expected: string): boolean {
|
||||
return (
|
||||
Array.isArray(value) &&
|
||||
value.some(item => typeof item === 'string' && item === expected)
|
||||
)
|
||||
}
|
||||
|
||||
function positiveInteger(value: unknown): number | undefined {
|
||||
return typeof value === 'number' &&
|
||||
Number.isInteger(value) &&
|
||||
value > 0
|
||||
? value
|
||||
: undefined
|
||||
}
|
||||
|
||||
export function mapLlmtrModel(raw: unknown) {
|
||||
if (!isRecord(raw)) {
|
||||
return null
|
||||
}
|
||||
|
||||
const id = typeof raw.id === 'string' ? raw.id.trim() : ''
|
||||
if (
|
||||
!id ||
|
||||
!hasStringValue(raw.supported_parameters, 'tools') ||
|
||||
!hasStringValue(raw.supported_operations, 'CHAT_COMPLETIONS') ||
|
||||
!hasStringValue(raw.supported_endpoints, '/v1/chat/completions')
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
const architecture = isRecord(raw.architecture) ? raw.architecture : null
|
||||
const topProvider = isRecord(raw.top_provider) ? raw.top_provider : null
|
||||
const contextWindow =
|
||||
positiveInteger(raw.context_length) ??
|
||||
positiveInteger(topProvider?.context_length)
|
||||
const maxOutputTokens = positiveInteger(
|
||||
topProvider?.max_completion_tokens,
|
||||
)
|
||||
|
||||
return {
|
||||
id,
|
||||
apiName: id,
|
||||
label:
|
||||
typeof raw.name === 'string' && raw.name.trim() ? raw.name.trim() : id,
|
||||
capabilities: {
|
||||
supportsFunctionCalling: true,
|
||||
supportsVision: hasStringValue(architecture?.input_modalities, 'image'),
|
||||
supportsReasoning:
|
||||
isRecord(raw.reasoning) ||
|
||||
hasStringValue(raw.supported_parameters, 'reasoning') ||
|
||||
hasStringValue(raw.supported_parameters, 'reasoning_effort'),
|
||||
},
|
||||
...(contextWindow ? { contextWindow } : {}),
|
||||
...(maxOutputTokens ? { maxOutputTokens } : {}),
|
||||
}
|
||||
}
|
||||
|
||||
export default defineCatalog({
|
||||
source: 'hybrid',
|
||||
discovery: {
|
||||
kind: 'openai-compatible',
|
||||
requiresAuth: false,
|
||||
mapModel: mapLlmtrModel,
|
||||
},
|
||||
discoveryCacheTtl: '1d',
|
||||
discoveryRefreshMode: 'background-if-stale',
|
||||
allowManualRefresh: true,
|
||||
models: [
|
||||
{
|
||||
id: 'llmtr-deepseek-v4-flash',
|
||||
apiName: 'deepseek/deepseek-v4-flash',
|
||||
aliases: ['deepseek-v4-flash'],
|
||||
label: 'DeepSeek V4 Flash',
|
||||
modelDescriptorId: 'deepseek-v4-flash',
|
||||
contextWindow: 1_000_000,
|
||||
maxOutputTokens: 393_216,
|
||||
},
|
||||
{
|
||||
id: 'llmtr-claude-sonnet-4.6',
|
||||
apiName: 'anthropic/claude-sonnet-4.6',
|
||||
aliases: ['claude-sonnet-4-6'],
|
||||
label: 'Claude Sonnet 4.6',
|
||||
modelDescriptorId: 'claude-sonnet-4-6',
|
||||
contextWindow: 1_000_000,
|
||||
maxOutputTokens: 128_000,
|
||||
},
|
||||
{
|
||||
id: 'llmtr-gpt-5.4',
|
||||
apiName: 'openai/gpt-5.4',
|
||||
aliases: ['gpt-5.4'],
|
||||
label: 'GPT-5.4',
|
||||
modelDescriptorId: 'gpt-5.4',
|
||||
contextWindow: 272_000,
|
||||
maxOutputTokens: 128_000,
|
||||
},
|
||||
{
|
||||
id: 'llmtr-gemini-3.1-pro-preview',
|
||||
apiName: 'google/gemini-3.1-pro-preview',
|
||||
label: 'Gemini 3.1 Pro Preview',
|
||||
modelDescriptorId: 'google/gemini-3.1-pro-preview',
|
||||
contextWindow: 1_048_576,
|
||||
maxOutputTokens: 65_536,
|
||||
},
|
||||
{
|
||||
id: 'llmtr-deepseek-v4-pro',
|
||||
apiName: 'deepseek/deepseek-v4-pro',
|
||||
aliases: ['deepseek-v4-pro'],
|
||||
label: 'DeepSeek V4 Pro',
|
||||
modelDescriptorId: 'deepseek-v4-pro',
|
||||
contextWindow: 1_000_000,
|
||||
maxOutputTokens: 393_216,
|
||||
},
|
||||
{
|
||||
id: 'llmtr-glm-5.2',
|
||||
apiName: 'zai/glm-5.2',
|
||||
aliases: ['glm-5.2'],
|
||||
label: 'GLM-5.2',
|
||||
modelDescriptorId: 'glm-5.2',
|
||||
contextWindow: 1_000_000,
|
||||
maxOutputTokens: 131_072,
|
||||
},
|
||||
],
|
||||
})
|
||||
@@ -0,0 +1,187 @@
|
||||
import { expect, test } from 'bun:test'
|
||||
|
||||
import { routeForPreset } from '../compatibility.js'
|
||||
import { getProviderPresetUiMetadata } from '../providerUiMetadata.js'
|
||||
import {
|
||||
resolveActiveRouteIdFromEnv,
|
||||
resolveRouteCredentialValue,
|
||||
resolveRouteIdFromBaseUrl,
|
||||
} from '../routeMetadata.js'
|
||||
import catalog, { mapLlmtrModel } from './llmtr.models.js'
|
||||
import gateway from './llmtr.js'
|
||||
|
||||
test('LLMTR uses the standard OpenAI-compatible gateway contract', () => {
|
||||
expect(gateway.defaultBaseUrl).toBe('https://llmtr.com/v1')
|
||||
expect(gateway.defaultModel).toBe('deepseek/deepseek-v4-flash')
|
||||
expect(gateway.setup.credentialEnvVars).toEqual([
|
||||
'LLMTR_API_KEY',
|
||||
'OPENAI_API_KEY',
|
||||
])
|
||||
expect(gateway.setup.dedicatedCredentialsOnly).not.toBe(true)
|
||||
expect(gateway.preset?.apiKeyEnvVars).toEqual([
|
||||
'LLMTR_API_KEY',
|
||||
'OPENAI_API_KEY',
|
||||
])
|
||||
expect(gateway.validation).toMatchObject({
|
||||
kind: 'credential-env',
|
||||
routing: { matchDefaultBaseUrl: true },
|
||||
credentialEnvVars: [
|
||||
'LLMTR_API_KEY',
|
||||
'OPENAI_API_KEYS',
|
||||
'OPENAI_API_KEY',
|
||||
],
|
||||
})
|
||||
expect(gateway.validation?.routing?.matchBaseUrlHosts).toBeUndefined()
|
||||
expect(gateway.transportConfig).toEqual({
|
||||
kind: 'openai-compatible',
|
||||
openaiShim: {
|
||||
requiredApiFormat: 'chat_completions',
|
||||
supportsAuthHeaders: false,
|
||||
maxTokensField: 'max_tokens',
|
||||
},
|
||||
})
|
||||
expect(catalog.source).toBe('hybrid')
|
||||
expect(catalog.discovery?.requiresAuth).toBe(false)
|
||||
})
|
||||
|
||||
test('LLMTR preset uses the existing generic profile path', () => {
|
||||
expect(routeForPreset('llmtr')).toEqual({
|
||||
vendorId: 'openai',
|
||||
gatewayId: 'llmtr',
|
||||
routeId: 'llmtr',
|
||||
})
|
||||
expect(
|
||||
getProviderPresetUiMetadata('llmtr', {
|
||||
LLMTR_API_KEY: 'llmtr-key',
|
||||
OPENAI_API_KEY: 'fallback-key',
|
||||
}),
|
||||
).toMatchObject({
|
||||
apiKey: 'llmtr-key',
|
||||
baseUrl: 'https://llmtr.com/v1',
|
||||
model: 'deepseek/deepseek-v4-flash',
|
||||
provider: 'llmtr',
|
||||
routeId: 'llmtr',
|
||||
})
|
||||
|
||||
expect(
|
||||
getProviderPresetUiMetadata('llmtr', {
|
||||
OPENAI_API_KEY: 'fallback-key',
|
||||
}).apiKey,
|
||||
).toBe('fallback-key')
|
||||
|
||||
expect(
|
||||
getProviderPresetUiMetadata('llmtr', {
|
||||
LLMTR_API_KEY: 'SUA_CHAVE',
|
||||
}).apiKey,
|
||||
).toBe('')
|
||||
expect(
|
||||
getProviderPresetUiMetadata('llmtr', {
|
||||
LLMTR_API_KEY: 'SUA_CHAVE',
|
||||
OPENAI_API_KEY: 'fallback-key',
|
||||
}).apiKey,
|
||||
).toBe('fallback-key')
|
||||
})
|
||||
|
||||
test('LLMTR dedicated credentials require an already selected LLMTR route', () => {
|
||||
const processEnv = { LLMTR_API_KEY: 'llmtr-key' }
|
||||
|
||||
expect(resolveRouteIdFromBaseUrl('https://llmtr.com/v1')).toBe('llmtr')
|
||||
expect(resolveRouteIdFromBaseUrl('https://llmtr.com/proxy/v1')).toBeNull()
|
||||
expect(
|
||||
resolveRouteIdFromBaseUrl('https://llmtr.com/v1?tenant=proxy'),
|
||||
).toBeNull()
|
||||
expect(resolveRouteIdFromBaseUrl('https://llmtr.com/v1#proxy')).toBeNull()
|
||||
expect(
|
||||
resolveRouteCredentialValue({
|
||||
routeId: 'llmtr',
|
||||
baseUrl: 'https://llmtr.com/v1',
|
||||
processEnv,
|
||||
}),
|
||||
).toBe('llmtr-key')
|
||||
expect(
|
||||
resolveRouteCredentialValue({
|
||||
routeId: 'custom',
|
||||
baseUrl: 'https://proxy.example/v1',
|
||||
processEnv,
|
||||
}),
|
||||
).toBeUndefined()
|
||||
expect(
|
||||
resolveRouteCredentialValue({
|
||||
routeId: 'llmtr',
|
||||
baseUrl: 'https://proxy.example/v1',
|
||||
processEnv,
|
||||
}),
|
||||
).toBeUndefined()
|
||||
expect(
|
||||
resolveActiveRouteIdFromEnv(
|
||||
{
|
||||
CLAUDE_CODE_USE_OPENAI: '1',
|
||||
OPENAI_BASE_URL: 'https://proxy.example/v1',
|
||||
},
|
||||
{
|
||||
activeProfileProvider: 'llmtr',
|
||||
activeProfileBaseUrl: 'https://proxy.example/v1',
|
||||
},
|
||||
),
|
||||
).toBe('custom')
|
||||
expect(
|
||||
resolveActiveRouteIdFromEnv(processEnv),
|
||||
).not.toBe('llmtr')
|
||||
})
|
||||
|
||||
test('LLMTR discovery keeps tool-capable Chat Completions models', () => {
|
||||
expect(
|
||||
mapLlmtrModel({
|
||||
id: 'example/chat-model',
|
||||
name: 'Chat Model',
|
||||
context_length: 131_072,
|
||||
top_provider: {
|
||||
max_completion_tokens: 32_768,
|
||||
},
|
||||
architecture: {
|
||||
input_modalities: ['text', 'image'],
|
||||
},
|
||||
reasoning: {},
|
||||
supported_parameters: ['tools', 'reasoning'],
|
||||
supported_operations: ['CHAT_COMPLETIONS'],
|
||||
supported_endpoints: ['/v1/chat/completions'],
|
||||
}),
|
||||
).toEqual({
|
||||
id: 'example/chat-model',
|
||||
apiName: 'example/chat-model',
|
||||
label: 'Chat Model',
|
||||
capabilities: {
|
||||
supportsFunctionCalling: true,
|
||||
supportsVision: true,
|
||||
supportsReasoning: true,
|
||||
},
|
||||
contextWindow: 131_072,
|
||||
maxOutputTokens: 32_768,
|
||||
})
|
||||
})
|
||||
|
||||
test.each([
|
||||
['Responses-only', ['RESPONSES'], ['/v1/responses'], ['tools']],
|
||||
['no tools', ['CHAT_COMPLETIONS'], ['/v1/chat/completions'], []],
|
||||
['wrong endpoint', ['CHAT_COMPLETIONS'], ['/v1/responses'], ['tools']],
|
||||
])('LLMTR discovery rejects %s models', (_label, operations, endpoints, parameters) => {
|
||||
expect(
|
||||
mapLlmtrModel({
|
||||
id: 'example/incompatible',
|
||||
supported_operations: operations,
|
||||
supported_endpoints: endpoints,
|
||||
supported_parameters: parameters,
|
||||
}),
|
||||
).toBeNull()
|
||||
})
|
||||
|
||||
test('LLMTR curated models are a small tool-capable fallback catalog', () => {
|
||||
expect(catalog.models?.map(model => model.apiName)).toEqual([
|
||||
'deepseek/deepseek-v4-flash',
|
||||
'anthropic/claude-sonnet-4.6',
|
||||
'openai/gpt-5.4',
|
||||
'google/gemini-3.1-pro-preview',
|
||||
'deepseek/deepseek-v4-pro',
|
||||
'zai/glm-5.2',
|
||||
])
|
||||
})
|
||||
@@ -0,0 +1,49 @@
|
||||
import { defineGateway } from '../define.js'
|
||||
import catalog from './llmtr.models.js'
|
||||
|
||||
export default defineGateway({
|
||||
id: 'llmtr',
|
||||
label: 'LLMTR',
|
||||
category: 'aggregating',
|
||||
defaultBaseUrl: 'https://llmtr.com/v1',
|
||||
defaultModel: 'deepseek/deepseek-v4-flash',
|
||||
supportsModelRouting: true,
|
||||
setup: {
|
||||
requiresAuth: true,
|
||||
authMode: 'api-key',
|
||||
credentialEnvVars: ['LLMTR_API_KEY', 'OPENAI_API_KEY'],
|
||||
},
|
||||
startup: {
|
||||
probeReadiness: 'openai-compatible-models',
|
||||
},
|
||||
transportConfig: {
|
||||
kind: 'openai-compatible',
|
||||
openaiShim: {
|
||||
requiredApiFormat: 'chat_completions',
|
||||
supportsAuthHeaders: false,
|
||||
maxTokensField: 'max_tokens',
|
||||
},
|
||||
},
|
||||
preset: {
|
||||
id: 'llmtr',
|
||||
description: 'LLMTR OpenAI-compatible multi-model gateway',
|
||||
vendorId: 'openai',
|
||||
apiKeyEnvVars: ['LLMTR_API_KEY', 'OPENAI_API_KEY'],
|
||||
modelEnvVars: ['OPENAI_MODEL'],
|
||||
},
|
||||
validation: {
|
||||
kind: 'credential-env',
|
||||
routing: {
|
||||
matchDefaultBaseUrl: true,
|
||||
},
|
||||
credentialEnvVars: [
|
||||
'LLMTR_API_KEY',
|
||||
'OPENAI_API_KEYS',
|
||||
'OPENAI_API_KEY',
|
||||
],
|
||||
missingCredentialMessage:
|
||||
'LLMTR auth is required. Set LLMTR_API_KEY or OPENAI_API_KEY.',
|
||||
},
|
||||
catalog,
|
||||
usage: { supported: false },
|
||||
})
|
||||
@@ -37,6 +37,7 @@ import gatewayGitlawbOpengateway from '../gateways/gitlawb-opengateway.js'
|
||||
import gatewayGroq from '../gateways/groq.js'
|
||||
import gatewayHicap from '../gateways/hicap.js'
|
||||
import gatewayKimiCode from '../gateways/kimi-code.js'
|
||||
import gatewayLlmtr from '../gateways/llmtr.js'
|
||||
import gatewayLmstudio from '../gateways/lmstudio.js'
|
||||
import gatewayMistral from '../gateways/mistral.js'
|
||||
import gatewayNvidiaNim from '../gateways/nvidia-nim.js'
|
||||
@@ -91,7 +92,7 @@ import modelXai from '../models/xai.js'
|
||||
import modelXiaomiMimo from '../models/xiaomi-mimo.js'
|
||||
|
||||
export const VENDOR_DESCRIPTORS = [vendorAnthropic, vendorBankr, vendorDeepseek, vendorFireworks, vendorGemini, vendorLongcat, vendorMinimax, vendorMoonshot, vendorNearai, vendorOpenai, vendorVenice, vendorXai, vendorXiaomiMimo, vendorZai] as const satisfies readonly VendorDescriptor[]
|
||||
export const GATEWAY_DESCRIPTORS = [gatewayAimlapi, gatewayApismart, gatewayAtlasCloud, gatewayAtomicChat, gatewayAzureOpenai, gatewayBedrock, gatewayClinepass, gatewayCloudflare, gatewayConcentrate, gatewayCustom, gatewayDashscopeCn, gatewayDashscopeIntl, gatewayGithubEnterprise, gatewayGithub, gatewayGitlawbOpengateway, gatewayGroq, gatewayHicap, gatewayKimiCode, gatewayLmstudio, gatewayMistral, gatewayNvidiaNim, gatewayOllama, gatewayOpencodeGo, gatewayOpencode, gatewayOpenrouter, gatewayTogether, gatewayVertex, gatewayXiaomiMimoToken] as const satisfies readonly GatewayDescriptor[]
|
||||
export const GATEWAY_DESCRIPTORS = [gatewayAimlapi, gatewayApismart, gatewayAtlasCloud, gatewayAtomicChat, gatewayAzureOpenai, gatewayBedrock, gatewayClinepass, gatewayCloudflare, gatewayConcentrate, gatewayCustom, gatewayDashscopeCn, gatewayDashscopeIntl, gatewayGithubEnterprise, gatewayGithub, gatewayGitlawbOpengateway, gatewayGroq, gatewayHicap, gatewayKimiCode, gatewayLlmtr, gatewayLmstudio, gatewayMistral, gatewayNvidiaNim, gatewayOllama, gatewayOpencodeGo, gatewayOpencode, gatewayOpenrouter, gatewayTogether, gatewayVertex, gatewayXiaomiMimoToken] as const satisfies readonly GatewayDescriptor[]
|
||||
export const ANTHROPIC_PROXY_DESCRIPTORS = [anthropicproxyCustom] as const satisfies readonly AnthropicProxyDescriptor[]
|
||||
export const BRAND_DESCRIPTORS = [brandClaude, brandDeepseek, brandFireworks, brandGemini, brandGlm, brandGpt, brandKimi, brandLing, brandLlama, brandLongcat, brandMacaron, brandMinimax, brandMistral, brandNearai, brandNemotron, brandOpenaiCompatibleAlias, brandQwen, brandTencent, brandXai, brandXiaomiMimo] as const satisfies readonly BrandDescriptor[]
|
||||
export const MODEL_DESCRIPTOR_GROUPS = [modelClaude, modelDeepseek, modelFireworksMerged, modelGemini, modelGlm, modelGpt, modelKimi, modelLing, modelLlama, modelLongcat, modelMacaron, modelMinimax, modelMistral, modelNearai, modelNemotron, modelOpenaiCompatibleAlias, modelOpencode, modelQwen, modelTencent, modelXai, modelXiaomiMimo] as const satisfies readonly (readonly ModelDescriptor[])[]
|
||||
|
||||
@@ -252,6 +252,21 @@ export const PROVIDER_PRESET_MANIFEST = [
|
||||
"OPENAI_MODEL"
|
||||
]
|
||||
},
|
||||
{
|
||||
"preset": "llmtr",
|
||||
"routeKind": "gateway",
|
||||
"routeId": "llmtr",
|
||||
"vendorId": "openai",
|
||||
"gatewayId": "llmtr",
|
||||
"description": "LLMTR OpenAI-compatible multi-model gateway",
|
||||
"apiKeyEnvVars": [
|
||||
"LLMTR_API_KEY",
|
||||
"OPENAI_API_KEY"
|
||||
],
|
||||
"modelEnvVars": [
|
||||
"OPENAI_MODEL"
|
||||
]
|
||||
},
|
||||
{
|
||||
"preset": "lmstudio",
|
||||
"routeKind": "gateway",
|
||||
@@ -572,6 +587,7 @@ export const ORDERED_PROVIDER_PRESETS = [
|
||||
"gemini",
|
||||
"groq",
|
||||
"hicap",
|
||||
"llmtr",
|
||||
"lmstudio",
|
||||
"atomic-chat",
|
||||
"ollama",
|
||||
|
||||
@@ -41,7 +41,11 @@ function hasUsableEnvValue(envVar: string, value: string | undefined): boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
if (envVar === 'OPENAI_API_KEYS' || envVar === 'OPENAI_API_KEY') {
|
||||
if (
|
||||
envVar === 'OPENAI_API_KEYS' ||
|
||||
envVar === 'OPENAI_API_KEY' ||
|
||||
envVar === 'LLMTR_API_KEY'
|
||||
) {
|
||||
return hasUsableOpenAICredential(value)
|
||||
}
|
||||
|
||||
|
||||
@@ -231,7 +231,8 @@ function hasUsableEnvCredentialValue(
|
||||
envVar === 'OPENAI_API_KEY' ||
|
||||
envVar === 'AIMLAPI_API_KEY' ||
|
||||
envVar === 'APISMART_API_KEY' ||
|
||||
envVar === 'CONCENTRATE_API_KEY'
|
||||
envVar === 'CONCENTRATE_API_KEY' ||
|
||||
envVar === 'LLMTR_API_KEY'
|
||||
) {
|
||||
return hasUsableOpenAICredential(value)
|
||||
}
|
||||
@@ -467,6 +468,34 @@ export function isCanonicalConcentrateInferenceBaseUrl(
|
||||
}
|
||||
}
|
||||
|
||||
const LLMTR_CANONICAL_INFERENCE_BASE_URL = 'https://llmtr.com/v1'
|
||||
|
||||
export function isCanonicalLlmtrInferenceBaseUrl(
|
||||
value: string | undefined,
|
||||
): boolean {
|
||||
const trimmed = value?.trim()
|
||||
if (!trimmed) {
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
const canonical = new URL(LLMTR_CANONICAL_INFERENCE_BASE_URL)
|
||||
const candidate = new URL(trimmed)
|
||||
const normalizePath = (pathname: string): string =>
|
||||
pathname.replace(/\/+$/, '') || '/'
|
||||
return (
|
||||
candidate.protocol === 'https:' &&
|
||||
!candidate.port &&
|
||||
!candidate.search &&
|
||||
!candidate.hash &&
|
||||
candidate.hostname.toLowerCase() === canonical.hostname.toLowerCase() &&
|
||||
normalizePath(candidate.pathname) === normalizePath(canonical.pathname)
|
||||
)
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export function getConcentrateBaseUrlOverride(
|
||||
processEnv: NodeJS.ProcessEnv = process.env,
|
||||
): string | undefined {
|
||||
@@ -1117,6 +1146,13 @@ export function resolveRouteCredentialValue(
|
||||
) {
|
||||
return undefined
|
||||
}
|
||||
if (
|
||||
routeId === 'llmtr' &&
|
||||
options?.baseUrl !== undefined &&
|
||||
!isCanonicalLlmtrInferenceBaseUrl(options.baseUrl)
|
||||
) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return getRouteCredentialValue(routeId, processEnv)
|
||||
}
|
||||
@@ -1225,6 +1261,16 @@ export function resolveRouteIdFromBaseUrl(
|
||||
normalizedBaseUrl &&
|
||||
normalizedDefaultBaseUrl === normalizedBaseUrl
|
||||
) {
|
||||
// LLMTR's dedicated credential and fixed transport contract are valid
|
||||
// only for the exact inference URL. The generic comparable-URL helper
|
||||
// intentionally ignores query/hash components for other providers, but a
|
||||
// query-bearing LLMTR URL is a retargeted/custom endpoint.
|
||||
if (
|
||||
route.id === 'llmtr' &&
|
||||
!isCanonicalLlmtrInferenceBaseUrl(baseUrl)
|
||||
) {
|
||||
continue
|
||||
}
|
||||
return route.id
|
||||
}
|
||||
}
|
||||
@@ -1241,7 +1287,9 @@ export function resolveRouteIdFromBaseUrl(
|
||||
(route.id === 'longcat' && !isLongcatBaseUrl(baseUrl)) ||
|
||||
(route.id === 'apismart' && !isApismartBaseUrl(baseUrl)) ||
|
||||
(route.id === 'concentrate' &&
|
||||
!isCanonicalConcentrateInferenceBaseUrl(baseUrl))
|
||||
!isCanonicalConcentrateInferenceBaseUrl(baseUrl)) ||
|
||||
(route.id === 'llmtr' &&
|
||||
!isCanonicalLlmtrInferenceBaseUrl(baseUrl))
|
||||
) {
|
||||
continue
|
||||
}
|
||||
@@ -1281,6 +1329,9 @@ function profileRouteHonorsBaseUrlBoundary(
|
||||
if (routeId === 'apismart') {
|
||||
return isApismartBaseUrl(baseUrl)
|
||||
}
|
||||
if (routeId === 'llmtr') {
|
||||
return isCanonicalLlmtrInferenceBaseUrl(baseUrl)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
@@ -293,6 +293,24 @@ describe('resolveModelRuntimeLimits', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('LLMTR runtime attribution', () => {
|
||||
it('keeps query-bearing endpoints on the generic custom transport', () => {
|
||||
const result = resolveOpenAIShimRuntimeContext({
|
||||
activeProfileProvider: 'llmtr',
|
||||
baseUrl: 'https://llmtr.com/v1?tenant=proxy',
|
||||
model: 'proxy-model',
|
||||
processEnv: {
|
||||
CLAUDE_CODE_USE_OPENAI: '1',
|
||||
OPENAI_API_FORMAT: 'responses',
|
||||
},
|
||||
})
|
||||
|
||||
expect(result.routeId).not.toBe('llmtr')
|
||||
expect(result.openaiShimConfig.requiredApiFormat).toBeUndefined()
|
||||
expect(result.openaiShimConfig.maxTokensField).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('AIMLAPI runtime attribution', () => {
|
||||
it('sends the fixed partner id on the canonical endpoint only', () => {
|
||||
const previous = process.env.AIMLAPI_PARTNER_ID
|
||||
|
||||
@@ -20,6 +20,7 @@ const originalEnv = {
|
||||
OPENAI_API_BASE: process.env.OPENAI_API_BASE,
|
||||
OPENAI_API_KEY: process.env.OPENAI_API_KEY,
|
||||
OPENAI_API_KEYS: process.env.OPENAI_API_KEYS,
|
||||
LLMTR_API_KEY: process.env.LLMTR_API_KEY,
|
||||
OPENAI_MODEL: process.env.OPENAI_MODEL,
|
||||
OPENAI_API_FORMAT: process.env.OPENAI_API_FORMAT,
|
||||
OPENAI_AZURE_STYLE: process.env.OPENAI_AZURE_STYLE,
|
||||
@@ -409,19 +410,25 @@ function makeChatCompletionResponse(model: string): Response {
|
||||
|
||||
async function captureChatCompletionRequest(
|
||||
model = 'mimo-v2.5-pro',
|
||||
): Promise<{ authorization: string | null; url: string | null }> {
|
||||
defaultHeaders: Record<string, string> = {},
|
||||
): Promise<{
|
||||
authorization: string | null
|
||||
headers: Record<string, string>
|
||||
url: string | null
|
||||
}> {
|
||||
let authorization: string | null = null
|
||||
let headers: Record<string, string> = {}
|
||||
let url: string | null = null
|
||||
|
||||
globalThis.fetch = (async (input, init) => {
|
||||
url = String(input)
|
||||
const headers = init?.headers as Record<string, string> | undefined
|
||||
authorization = headers?.Authorization ?? headers?.authorization ?? null
|
||||
headers = (init?.headers as Record<string, string> | undefined) ?? {}
|
||||
authorization = headers.Authorization ?? headers.authorization ?? null
|
||||
|
||||
return makeChatCompletionResponse(model)
|
||||
}) as unknown as FetchType
|
||||
|
||||
const client = createOpenAIShimClient({}) as OpenAIShimClient
|
||||
const client = createOpenAIShimClient({ defaultHeaders }) as OpenAIShimClient
|
||||
|
||||
await client.beta.messages.create({
|
||||
model,
|
||||
@@ -430,7 +437,7 @@ async function captureChatCompletionRequest(
|
||||
stream: false,
|
||||
})
|
||||
|
||||
return { authorization, url }
|
||||
return { authorization, headers, url }
|
||||
}
|
||||
|
||||
function makeCodexSseResponse(responseData: Record<string, unknown>): Response {
|
||||
@@ -444,6 +451,7 @@ beforeEach(async () => {
|
||||
delete process.env.OPENAI_API_BASE
|
||||
process.env.OPENAI_API_KEY = 'test-key'
|
||||
delete process.env.OPENAI_API_KEYS
|
||||
delete process.env.LLMTR_API_KEY
|
||||
delete process.env.OPENAI_MODEL
|
||||
delete process.env.OPENAI_API_FORMAT
|
||||
delete process.env.OPENAI_AZURE_STYLE
|
||||
@@ -493,6 +501,7 @@ afterEach(() => {
|
||||
restoreEnv('OPENAI_API_BASE', originalEnv.OPENAI_API_BASE)
|
||||
restoreEnv('OPENAI_API_KEY', originalEnv.OPENAI_API_KEY)
|
||||
restoreEnv('OPENAI_API_KEYS', originalEnv.OPENAI_API_KEYS)
|
||||
restoreEnv('LLMTR_API_KEY', originalEnv.LLMTR_API_KEY)
|
||||
restoreEnv('OPENAI_MODEL', originalEnv.OPENAI_MODEL)
|
||||
restoreEnv('OPENAI_API_FORMAT', originalEnv.OPENAI_API_FORMAT)
|
||||
restoreEnv('OPENAI_AZURE_STYLE', originalEnv.OPENAI_AZURE_STYLE)
|
||||
@@ -570,6 +579,86 @@ test('Concentrate selection prefers its dedicated key over a generic OPENAI_API_
|
||||
expect(captured.authorization).toBe('Bearer concentrate-key')
|
||||
})
|
||||
|
||||
test('selected LLMTR route sends LLMTR_API_KEY through the generic route credential resolver', async () => {
|
||||
process.env.LLMTR_API_KEY = 'llmtr-key'
|
||||
delete process.env.OPENAI_API_KEYS
|
||||
delete process.env.OPENAI_API_KEY
|
||||
delete process.env.OPENAI_BASE_URL
|
||||
delete process.env.OPENAI_MODEL
|
||||
|
||||
const result = applyProviderFlag('llmtr', [])
|
||||
expect(result.error).toBeUndefined()
|
||||
|
||||
const captured = await captureChatCompletionRequest(
|
||||
'deepseek/deepseek-v4-flash',
|
||||
)
|
||||
|
||||
expect(captured.url).toBe('https://llmtr.com/v1/chat/completions')
|
||||
expect(captured.authorization).toBe('Bearer llmtr-key')
|
||||
})
|
||||
|
||||
test('selected LLMTR route prefers its dedicated key over a generic OPENAI_API_KEYS pool', async () => {
|
||||
process.env.LLMTR_API_KEY = 'llmtr-key'
|
||||
process.env.OPENAI_API_KEYS = 'generic-openai-key-a,generic-openai-key-b'
|
||||
delete process.env.OPENAI_API_KEY
|
||||
delete process.env.OPENAI_BASE_URL
|
||||
delete process.env.OPENAI_MODEL
|
||||
|
||||
const result = applyProviderFlag('llmtr', [])
|
||||
expect(result.error).toBeUndefined()
|
||||
|
||||
const captured = await captureChatCompletionRequest(
|
||||
'deepseek/deepseek-v4-flash',
|
||||
)
|
||||
|
||||
expect(captured.authorization).toBe('Bearer llmtr-key')
|
||||
})
|
||||
|
||||
test('raw-env LLMTR ignores unsupported custom auth and custom headers', async () => {
|
||||
process.env.CLAUDE_CODE_USE_OPENAI = '1'
|
||||
process.env.OPENAI_BASE_URL = 'https://llmtr.com/v1'
|
||||
process.env.OPENAI_MODEL = 'deepseek/deepseek-v4-flash'
|
||||
process.env.LLMTR_API_KEY = 'llmtr-key'
|
||||
process.env.OPENAI_AUTH_HEADER = 'X-Proxy-Key'
|
||||
process.env.OPENAI_AUTH_SCHEME = 'raw'
|
||||
process.env.OPENAI_AUTH_HEADER_VALUE = 'proxy-secret'
|
||||
process.env.ANTHROPIC_CUSTOM_HEADERS = 'X-Tenant-Secret: tenant-secret'
|
||||
delete process.env.OPENAI_API_KEYS
|
||||
delete process.env.OPENAI_API_KEY
|
||||
|
||||
const captured = await captureChatCompletionRequest(
|
||||
'deepseek/deepseek-v4-flash',
|
||||
{ 'X-Tenant-Secret': 'tenant-secret' },
|
||||
)
|
||||
|
||||
expect(captured.url).toBe('https://llmtr.com/v1/chat/completions')
|
||||
expect(captured.authorization).toBe('Bearer llmtr-key')
|
||||
expect(captured.headers['X-Proxy-Key']).toBeUndefined()
|
||||
expect(captured.headers['X-Tenant-Secret']).toBeUndefined()
|
||||
})
|
||||
|
||||
test('custom endpoints preserve configured auth and custom headers', async () => {
|
||||
process.env.CLAUDE_CODE_USE_OPENAI = '1'
|
||||
process.env.OPENAI_BASE_URL = 'https://proxy.example/v1'
|
||||
process.env.OPENAI_MODEL = 'proxy-model'
|
||||
process.env.LLMTR_API_KEY = 'llmtr-key'
|
||||
process.env.OPENAI_AUTH_HEADER = 'X-Proxy-Key'
|
||||
process.env.OPENAI_AUTH_SCHEME = 'raw'
|
||||
process.env.OPENAI_AUTH_HEADER_VALUE = 'proxy-secret'
|
||||
process.env.ANTHROPIC_CUSTOM_HEADERS = 'X-Tenant-Secret: tenant-secret'
|
||||
delete process.env.OPENAI_API_KEYS
|
||||
delete process.env.OPENAI_API_KEY
|
||||
|
||||
const captured = await captureChatCompletionRequest('proxy-model', {
|
||||
'X-Tenant-Secret': 'tenant-secret',
|
||||
})
|
||||
|
||||
expect(captured.url).toBe('https://proxy.example/v1/chat/completions')
|
||||
expect(captured.authorization).toBeNull()
|
||||
expect(captured.headers['X-Proxy-Key']).toBe('proxy-secret')
|
||||
expect(captured.headers['X-Tenant-Secret']).toBe('tenant-secret')
|
||||
})
|
||||
|
||||
test('gitlawb opengateway provider flag uses generic OPENAI_API_KEYS pool before generic OPENAI_API_KEY fallback', async () => {
|
||||
process.env.OPENGATEWAY_BASE_URL = 'http://localhost:8181/v1'
|
||||
process.env.OPENAI_API_KEYS = 'fake-openai-pool-a,fake-openai-pool-b'
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
redactEncodedSecretSubstringsForDisplay,
|
||||
redactSecretSubstringsForDisplay,
|
||||
} from '../../../utils/providerSecrets.js'
|
||||
import { parseCustomHeadersEnv } from '../../../utils/providerCustomHeaders.js'
|
||||
|
||||
export function formatRetryAfterHint(response: Response): string {
|
||||
const retryAfter = response.headers.get('retry-after')
|
||||
@@ -240,11 +241,35 @@ export async function executeOpenAIRequest(
|
||||
isGithubCopilot,
|
||||
isGithubModels,
|
||||
} = context
|
||||
// Existing routes historically accept process-level custom auth even when
|
||||
// their profile UI hides those controls. LLMTR is the new fixed-contract
|
||||
// route: enforce its explicit capability without changing that compatibility
|
||||
// behavior for unrelated providers or generic custom endpoints.
|
||||
const supportsConfiguredAuthHeaders =
|
||||
runtimeShimContext.routeId !== 'llmtr' ||
|
||||
runtimeShimContext.openaiShimConfig.supportsAuthHeaders === true
|
||||
const unsupportedCustomHeaderNames = supportsConfiguredAuthHeaders
|
||||
? null
|
||||
: new Set(
|
||||
Object.keys(
|
||||
parseCustomHeadersEnv(
|
||||
requestProcessEnv.ANTHROPIC_CUSTOM_HEADERS,
|
||||
) ?? {},
|
||||
).map(name => name.toLowerCase()),
|
||||
)
|
||||
const filterUnsupportedCustomHeaders = (
|
||||
headers: Record<string, string> | undefined,
|
||||
): Record<string, string> =>
|
||||
Object.fromEntries(
|
||||
Object.entries(headers ?? {}).filter(
|
||||
([name]) => !unsupportedCustomHeaderNames?.has(name.toLowerCase()),
|
||||
),
|
||||
)
|
||||
const baseHeaders: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
...filterAnthropicHeaders(shimConfig.headers),
|
||||
...defaultHeaders,
|
||||
...filterAnthropicHeaders(options?.headers),
|
||||
...filterUnsupportedCustomHeaders(defaultHeaders),
|
||||
...filterUnsupportedCustomHeaders(filterAnthropicHeaders(options?.headers)),
|
||||
}
|
||||
|
||||
const isGemini = isGeminiMode()
|
||||
@@ -292,6 +317,7 @@ export async function executeOpenAIRequest(
|
||||
requestProcessEnv.NEARAI_API_KEY,
|
||||
requestProcessEnv.FIREWORKS_API_KEY,
|
||||
requestProcessEnv.LONGCAT_API_KEY,
|
||||
requestProcessEnv.LLMTR_API_KEY,
|
||||
].some((value) => value?.trim() === openAIApiKeyRawUsable),
|
||||
)
|
||||
const routeCredentialIsCopiedProviderKey = Boolean(
|
||||
@@ -330,7 +356,8 @@ export async function executeOpenAIRequest(
|
||||
const catalogAuthHeader =
|
||||
runtimeShimContext.catalogEntry?.transportOverrides?.openaiShim
|
||||
?.defaultAuthHeader
|
||||
const configuredAuthHeaderValue = catalogAuthHeader
|
||||
const configuredAuthHeaderValue =
|
||||
catalogAuthHeader || !supportsConfiguredAuthHeaders
|
||||
? undefined
|
||||
: requestProcessEnv.OPENAI_AUTH_HEADER_VALUE?.trim()
|
||||
if (configuredAuthHeaderValue && /[\r\n]/.test(configuredAuthHeaderValue)) {
|
||||
@@ -338,7 +365,8 @@ export async function executeOpenAIRequest(
|
||||
'OPENAI_AUTH_HEADER_VALUE must not contain CR/LF characters',
|
||||
)
|
||||
}
|
||||
const customAuthHeader = catalogAuthHeader
|
||||
const customAuthHeader =
|
||||
catalogAuthHeader || !supportsConfiguredAuthHeaders
|
||||
? undefined
|
||||
: requestProcessEnv.OPENAI_AUTH_HEADER?.trim()
|
||||
const hasCustomAuthHeader = Boolean(
|
||||
|
||||
@@ -18,6 +18,7 @@ const TEST_ENV_KEYS = [
|
||||
'CLAUDE_CODE_USE_OPENAI',
|
||||
'CODEX_AUTH_JSON_PATH',
|
||||
'CODEX_HOME',
|
||||
'LLMTR_API_KEY',
|
||||
'APISMART_API_KEY',
|
||||
'APISMART_MODEL',
|
||||
'OPENAI_API_KEYS',
|
||||
@@ -292,6 +293,17 @@ describe('loadEnvFile', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('loads the dedicated LLMTR credential without selecting a route', () => {
|
||||
const filePath = writeTempEnvFile('LLMTR_API_KEY=llmtr-key')
|
||||
|
||||
const loaded = loadEnvFile(filePath)
|
||||
|
||||
expect(process.env.LLMTR_API_KEY).toBe('llmtr-key')
|
||||
expect(process.env.CLAUDE_CODE_USE_OPENAI).toBeUndefined()
|
||||
expect(process.env.OPENAI_BASE_URL).toBeUndefined()
|
||||
expect(loaded).toEqual({ LLMTR_API_KEY: 'llmtr-key' })
|
||||
})
|
||||
|
||||
it('loads documented Concentrate env-only provider setup values', () => {
|
||||
const filePath = writeTempEnvFile([
|
||||
'CONCENTRATE_API_KEY=concentrate-key',
|
||||
|
||||
@@ -77,6 +77,7 @@ const ALLOWED_ENV_FILE_KEYS = new Set([
|
||||
'JINA_API_KEY',
|
||||
'KIMI_API_KEY',
|
||||
'LINKUP_API_KEY',
|
||||
'LLMTR_API_KEY',
|
||||
'LONGCAT_API_KEY',
|
||||
'MINIMAX_API_KEY',
|
||||
'MINIMAX_BASE_URL',
|
||||
|
||||
@@ -32,6 +32,7 @@ const ENV_KEYS = [
|
||||
'OPENAI_AUTH_HEADER',
|
||||
'OPENAI_AUTH_SCHEME',
|
||||
'OPENAI_AUTH_HEADER_VALUE',
|
||||
'LLMTR_API_KEY',
|
||||
'GEMINI_MODEL',
|
||||
'NVIDIA_API_KEY',
|
||||
'NVIDIA_NIM',
|
||||
@@ -574,6 +575,71 @@ describe('applyProviderFlag - descriptor-backed openai-compatible routes', () =>
|
||||
expect(process.env.OPENAI_BASE_URL).toBe('http://proxy.local:8080/v1')
|
||||
})
|
||||
|
||||
test('LLMTR clears stale custom auth only on its canonical endpoint', () => {
|
||||
process.env.LLMTR_API_KEY = 'llmtr-key'
|
||||
process.env.OPENAI_API_FORMAT = 'responses'
|
||||
process.env.OPENAI_AUTH_HEADER = 'X-Proxy-Key'
|
||||
process.env.OPENAI_AUTH_SCHEME = 'raw'
|
||||
process.env.OPENAI_AUTH_HEADER_VALUE = 'proxy-secret'
|
||||
process.env.ANTHROPIC_CUSTOM_HEADERS = 'X-Tenant-Secret: tenant-secret'
|
||||
|
||||
const result = applyProviderFlag('llmtr', [])
|
||||
|
||||
expect(result.error).toBeUndefined()
|
||||
expect(process.env.OPENAI_BASE_URL).toBe('https://llmtr.com/v1')
|
||||
expect(process.env.OPENAI_API_FORMAT).toBeUndefined()
|
||||
expect(process.env.OPENAI_AUTH_HEADER).toBeUndefined()
|
||||
expect(process.env.OPENAI_AUTH_SCHEME).toBeUndefined()
|
||||
expect(process.env.OPENAI_AUTH_HEADER_VALUE).toBeUndefined()
|
||||
expect(process.env.ANTHROPIC_CUSTOM_HEADERS).toBeUndefined()
|
||||
})
|
||||
|
||||
test('LLMTR preserves custom endpoint auth settings when the endpoint is explicit', () => {
|
||||
process.env.OPENAI_BASE_URL = 'https://proxy.example/v1'
|
||||
process.env.OPENAI_API_FORMAT = 'responses'
|
||||
process.env.OPENAI_AUTH_HEADER = 'X-Proxy-Key'
|
||||
process.env.OPENAI_AUTH_SCHEME = 'raw'
|
||||
process.env.OPENAI_AUTH_HEADER_VALUE = 'proxy-secret'
|
||||
process.env.ANTHROPIC_CUSTOM_HEADERS = 'X-Tenant-Secret: tenant-secret'
|
||||
|
||||
const result = applyProviderFlag('llmtr', [])
|
||||
|
||||
expect(result.error).toBeUndefined()
|
||||
expect(process.env.OPENAI_BASE_URL).toBe('https://proxy.example/v1')
|
||||
expect(process.env.OPENAI_API_FORMAT).toBe('responses')
|
||||
expect(process.env.OPENAI_AUTH_HEADER).toBe('X-Proxy-Key')
|
||||
expect(process.env.OPENAI_AUTH_SCHEME).toBe('raw')
|
||||
expect(process.env.OPENAI_AUTH_HEADER_VALUE).toBe('proxy-secret')
|
||||
expect(process.env.ANTHROPIC_CUSTOM_HEADERS).toBe(
|
||||
'X-Tenant-Secret: tenant-secret',
|
||||
)
|
||||
})
|
||||
|
||||
test('clears an LLMTR key copied into OPENAI_API_KEY when switching routes', () => {
|
||||
process.env.LLMTR_API_KEY = 'llmtr-secret'
|
||||
process.env.OPENAI_API_KEY = 'llmtr-secret'
|
||||
process.env.OPENAI_BASE_URL = 'https://llmtr.com/v1'
|
||||
|
||||
const result = applyProviderFlag('openrouter', [])
|
||||
|
||||
expect(result.error).toBeUndefined()
|
||||
expect(process.env.OPENAI_BASE_URL).toBe('https://openrouter.ai/api/v1')
|
||||
expect(process.env.OPENAI_API_KEY).toBeUndefined()
|
||||
expect(process.env.LLMTR_API_KEY).toBe('llmtr-secret')
|
||||
})
|
||||
|
||||
test('preserves an independent generic OPENAI_API_KEY when switching from LLMTR', () => {
|
||||
process.env.LLMTR_API_KEY = 'llmtr-secret'
|
||||
process.env.OPENAI_API_KEY = 'generic-openai-secret'
|
||||
process.env.OPENAI_BASE_URL = 'https://llmtr.com/v1'
|
||||
|
||||
const result = applyProviderFlag('openrouter', [])
|
||||
|
||||
expect(result.error).toBeUndefined()
|
||||
expect(process.env.OPENAI_BASE_URL).toBe('https://openrouter.ai/api/v1')
|
||||
expect(process.env.OPENAI_API_KEY).toBe('generic-openai-secret')
|
||||
})
|
||||
|
||||
test('descriptor-backed provider selection preserves custom OPENAI_API_BASE alias', () => {
|
||||
process.env.OPENAI_API_BASE = 'http://proxy.local:8080/v1'
|
||||
process.env.OPENGATEWAY_API_KEY = 'fake-ogw-key'
|
||||
|
||||
@@ -32,6 +32,7 @@ import { PRESET_VENDOR_MAP } from '../integrations/compatibility.js'
|
||||
import {
|
||||
isCanonicalApismartInferenceBaseUrl,
|
||||
isCanonicalConcentrateInferenceBaseUrl,
|
||||
isCanonicalLlmtrInferenceBaseUrl,
|
||||
} from '../integrations/routeMetadata.js'
|
||||
import { hasUsableOpenAICredential } from '../services/api/credentialPool.js'
|
||||
import { isFirstPartyAnthropicBaseUrlForEnv } from './anthropicBaseUrl.js'
|
||||
@@ -364,6 +365,9 @@ export function applyProviderFlag(
|
||||
: process.env.OPENAI_API_KEY !== undefined &&
|
||||
process.env.OPENAI_API_KEY === process.env.CONCENTRATE_API_KEY
|
||||
? 'concentrate'
|
||||
: process.env.OPENAI_API_KEY !== undefined &&
|
||||
process.env.OPENAI_API_KEY === process.env.LLMTR_API_KEY
|
||||
? 'llmtr'
|
||||
: process.env.OPENAI_API_KEY !== undefined &&
|
||||
process.env.OPENAI_API_KEY === process.env.NEARAI_API_KEY
|
||||
? 'nearai'
|
||||
@@ -837,6 +841,13 @@ export function applyProviderFlag(
|
||||
default:
|
||||
process.env.CLAUDE_CODE_USE_OPENAI = '1'
|
||||
applyOpenAIBaseUrlDefault(provider, defaultBaseUrl)
|
||||
if (
|
||||
provider === 'llmtr' &&
|
||||
isCanonicalLlmtrInferenceBaseUrl(getConfiguredOpenAIBaseUrl())
|
||||
) {
|
||||
clearUnsupportedOpenAIShimSettings('llmtr')
|
||||
delete process.env.ANTHROPIC_CUSTOM_HEADERS
|
||||
}
|
||||
if (defaultModel) {
|
||||
process.env.OPENAI_MODEL ??= defaultModel
|
||||
}
|
||||
|
||||
@@ -3148,6 +3148,44 @@ test('openai launch withholds ambient Concentrate credentials from a keyless pro
|
||||
assert.equal(canonical.CONCENTRATE_API_KEY, 'ambient-concentrate-key')
|
||||
})
|
||||
|
||||
test('openai launch withholds ambient LLMTR credentials from a keyless proxy profile on restart', async () => {
|
||||
const env = await buildLaunchEnv({
|
||||
profile: 'openai',
|
||||
persisted: profile('openai', {
|
||||
CLAUDE_CODE_PROVIDER_ROUTE_ID: 'llmtr',
|
||||
OPENAI_BASE_URL: 'https://proxy.example.com/v1',
|
||||
OPENAI_MODEL: 'proxy-model',
|
||||
}),
|
||||
goal: 'coding',
|
||||
processEnv: {
|
||||
OPENAI_BASE_URL: 'https://proxy.example.com/v1',
|
||||
OPENAI_API_KEY: 'ambient-llmtr-key',
|
||||
LLMTR_API_KEY: 'ambient-llmtr-key',
|
||||
},
|
||||
})
|
||||
|
||||
assert.equal(env.CLAUDE_CODE_PROVIDER_ROUTE_ID, 'llmtr')
|
||||
assert.equal(env.OPENAI_API_KEY, undefined)
|
||||
assert.equal(env.LLMTR_API_KEY, undefined)
|
||||
|
||||
const canonical = await buildLaunchEnv({
|
||||
profile: 'openai',
|
||||
persisted: profile('openai', {
|
||||
CLAUDE_CODE_PROVIDER_ROUTE_ID: 'llmtr',
|
||||
OPENAI_BASE_URL: 'https://llmtr.com/v1',
|
||||
OPENAI_MODEL: 'deepseek/deepseek-v4-flash',
|
||||
}),
|
||||
goal: 'coding',
|
||||
processEnv: {
|
||||
OPENAI_BASE_URL: 'https://llmtr.com/v1',
|
||||
LLMTR_API_KEY: 'ambient-llmtr-key',
|
||||
},
|
||||
})
|
||||
|
||||
assert.equal(canonical.OPENAI_API_KEY, undefined)
|
||||
assert.equal(canonical.LLMTR_API_KEY, 'ambient-llmtr-key')
|
||||
})
|
||||
|
||||
test('openai launch removes a legacy persisted Concentrate key from a noncanonical URL', async () => {
|
||||
const env = await buildLaunchEnv({
|
||||
profile: 'openai',
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
getRouteDefaultModel,
|
||||
isCanonicalApismartInferenceBaseUrl,
|
||||
isCanonicalConcentrateInferenceBaseUrl,
|
||||
isCanonicalLlmtrInferenceBaseUrl,
|
||||
isLongcatBaseUrl,
|
||||
normalizeXiaomiMimoBaseUrl,
|
||||
resolveRouteCredentialValue,
|
||||
@@ -118,6 +119,7 @@ const PROFILE_ENV_KEYS = [
|
||||
'NEARAI_API_KEY',
|
||||
'FIREWORKS_API_KEY',
|
||||
'LONGCAT_API_KEY',
|
||||
'LLMTR_API_KEY',
|
||||
'CONCENTRATE_API_KEY',
|
||||
'CONCENTRATE_BASE_URL',
|
||||
'CONCENTRATE_MODEL',
|
||||
@@ -208,6 +210,7 @@ export type ProfileEnv = {
|
||||
NEARAI_API_KEY?: string
|
||||
FIREWORKS_API_KEY?: string
|
||||
LONGCAT_API_KEY?: string
|
||||
LLMTR_API_KEY?: string
|
||||
CONCENTRATE_API_KEY?: string
|
||||
CONCENTRATE_BASE_URL?: string
|
||||
CONCENTRATE_MODEL?: string
|
||||
@@ -2147,8 +2150,15 @@ export async function buildLaunchEnv(options: {
|
||||
effectiveOpenAIRouteId === 'concentrate' &&
|
||||
!!env.OPENAI_BASE_URL?.trim() &&
|
||||
!isCanonicalConcentrateInferenceBaseUrl(env.OPENAI_BASE_URL)
|
||||
const isNoncanonicalLlmtrLaunch =
|
||||
effectiveOpenAIRouteId === 'llmtr' &&
|
||||
!!env.OPENAI_BASE_URL?.trim() &&
|
||||
!isCanonicalLlmtrInferenceBaseUrl(env.OPENAI_BASE_URL)
|
||||
const isNoncanonicalDedicatedOpenAILaunch =
|
||||
isNoncanonicalAimlapiLaunch || isNoncanonicalApismartLaunch || isNoncanonicalConcentrateLaunch
|
||||
isNoncanonicalAimlapiLaunch ||
|
||||
isNoncanonicalApismartLaunch ||
|
||||
isNoncanonicalConcentrateLaunch ||
|
||||
isNoncanonicalLlmtrLaunch
|
||||
if (isNoncanonicalDedicatedOpenAILaunch) {
|
||||
delete env.OPENAI_API_KEY
|
||||
delete env.OPENAI_API_KEYS
|
||||
@@ -2157,7 +2167,7 @@ export async function buildLaunchEnv(options: {
|
||||
// dedicated credential is never valid off the canonical endpoint, and
|
||||
// older profiles could have stored that same secret under either generic
|
||||
// alias. Do not resurrect it for a noncanonical Concentrate launch.
|
||||
if (!isNoncanonicalConcentrateLaunch) {
|
||||
if (!isNoncanonicalConcentrateLaunch && !isNoncanonicalLlmtrLaunch) {
|
||||
const persistedCredential = resolveOpenAICredentialEnvSelection(persistedEnv)
|
||||
if (persistedCredential) {
|
||||
env[persistedCredential.envVar] = persistedCredential.value
|
||||
@@ -2190,6 +2200,7 @@ export async function buildLaunchEnv(options: {
|
||||
'ATLAS_CLOUD_API_KEY',
|
||||
'APISMART_API_KEY',
|
||||
'CONCENTRATE_API_KEY',
|
||||
'LLMTR_API_KEY',
|
||||
'NEARAI_API_KEY',
|
||||
'FIREWORKS_API_KEY',
|
||||
'LONGCAT_API_KEY',
|
||||
@@ -2211,6 +2222,9 @@ export async function buildLaunchEnv(options: {
|
||||
if (dedicatedKey === 'CONCENTRATE_API_KEY' && effectiveOpenAIRouteId !== 'concentrate') {
|
||||
continue
|
||||
}
|
||||
if (dedicatedKey === 'LLMTR_API_KEY' && effectiveOpenAIRouteId !== 'llmtr') {
|
||||
continue
|
||||
}
|
||||
if (dedicatedKey === 'NVIDIA_API_KEY' && effectiveOpenAIRouteId !== 'nvidia-nim') {
|
||||
continue
|
||||
}
|
||||
@@ -2238,14 +2252,21 @@ export async function buildLaunchEnv(options: {
|
||||
dedicatedKey === 'CONCENTRATE_API_KEY' &&
|
||||
!!dedicatedBaseUrl &&
|
||||
!isCanonicalConcentrateInferenceBaseUrl(dedicatedBaseUrl)
|
||||
const withholdAmbientLlmtrKey =
|
||||
dedicatedKey === 'LLMTR_API_KEY' &&
|
||||
!!dedicatedBaseUrl &&
|
||||
!isCanonicalLlmtrInferenceBaseUrl(dedicatedBaseUrl)
|
||||
const withholdAmbientDedicatedKey =
|
||||
withholdAmbientAimlapiKey || withholdAmbientApismartKey || withholdAmbientConcentrateKey
|
||||
withholdAmbientAimlapiKey ||
|
||||
withholdAmbientApismartKey ||
|
||||
withholdAmbientConcentrateKey ||
|
||||
withholdAmbientLlmtrKey
|
||||
// Unlike the generic proxy-compatible routes above, Concentrate's
|
||||
// dedicated key is never valid outside its canonical inference endpoint.
|
||||
// Do not preserve a legacy persisted key for a retargeted Concentrate
|
||||
// profile: older versions could have serialized one before this boundary
|
||||
// was enforced.
|
||||
if (withholdAmbientConcentrateKey) {
|
||||
if (withholdAmbientConcentrateKey || withholdAmbientLlmtrKey) {
|
||||
continue
|
||||
}
|
||||
// AIMLAPI accepts generic OpenAI credentials, but ApiSmart is
|
||||
@@ -2278,9 +2299,23 @@ export async function buildLaunchEnv(options: {
|
||||
persistedOpenAICredential?.kind === 'usable'
|
||||
? sanitizeApiKey(persistedOpenAICredential.value)
|
||||
: undefined
|
||||
// A selected canonical LLMTR profile owns its saved credential. Prefer it
|
||||
// over an unrelated ambient LLMTR_API_KEY on restart, and migrate startup
|
||||
// files written before LLMTR_API_KEY was persisted explicitly.
|
||||
const persistedLlmtrProfileKey =
|
||||
dedicatedKey === 'LLMTR_API_KEY' &&
|
||||
effectiveOpenAIRouteId === 'llmtr' &&
|
||||
!!dedicatedBaseUrl &&
|
||||
isCanonicalLlmtrInferenceBaseUrl(dedicatedBaseUrl)
|
||||
? sanitizeApiKey(persistedEnv.LLMTR_API_KEY) ||
|
||||
(persistedOpenAICredential?.kind === 'usable'
|
||||
? sanitizeApiKey(persistedOpenAICredential.value)
|
||||
: undefined)
|
||||
: undefined
|
||||
const dedicatedValue = withholdAmbientDedicatedKey
|
||||
? sanitizeApiKey(persistedEnv[dedicatedKey])
|
||||
: backfillDedicatedFromOpenAI ||
|
||||
persistedLlmtrProfileKey ||
|
||||
sanitizeApiKey(processEnv[dedicatedKey]) ||
|
||||
sanitizeApiKey(persistedEnv[dedicatedKey]) ||
|
||||
backfillLegacyApismartProfileKey ||
|
||||
@@ -2390,10 +2425,16 @@ export async function buildStartupEnvFromProfile(options?: {
|
||||
persisted.env.CLAUDE_CODE_PROVIDER_ROUTE_ID === 'concentrate' &&
|
||||
!!persisted.env.OPENAI_BASE_URL?.trim() &&
|
||||
!isCanonicalConcentrateInferenceBaseUrl(persisted.env.OPENAI_BASE_URL)
|
||||
const persistedLlmtrProxy =
|
||||
persisted?.profile === 'openai' &&
|
||||
persisted.env.CLAUDE_CODE_PROVIDER_ROUTE_ID === 'llmtr' &&
|
||||
!!persisted.env.OPENAI_BASE_URL?.trim() &&
|
||||
!isCanonicalLlmtrInferenceBaseUrl(persisted.env.OPENAI_BASE_URL)
|
||||
if (
|
||||
hasConcreteProviderSelection(processEnv) &&
|
||||
!persistedApismartProxy &&
|
||||
!persistedConcentrateProxy
|
||||
!persistedConcentrateProxy &&
|
||||
!persistedLlmtrProxy
|
||||
) {
|
||||
return processEnv
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { join } from 'node:path'
|
||||
import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test'
|
||||
|
||||
import { acquireEnvMutex, releaseEnvMutex } from '../entrypoints/sdk/shared.js'
|
||||
import { resolveRouteCredentialValue } from '../integrations/routeMetadata.js'
|
||||
import type { ProviderProfile } from './config.js'
|
||||
|
||||
async function importFreshProvidersModule() {
|
||||
@@ -72,6 +73,7 @@ const RESTORED_KEYS = [
|
||||
'ATLAS_CLOUD_API_KEY',
|
||||
'APISMART_API_KEY',
|
||||
'APISMART_MODEL',
|
||||
'LLMTR_API_KEY',
|
||||
'CONCENTRATE_API_KEY',
|
||||
'CONCENTRATE_BASE_URL',
|
||||
'CONCENTRATE_MODEL',
|
||||
@@ -292,6 +294,17 @@ function buildConcentrateProfile(overrides: Partial<ProviderProfile> = {}): Prov
|
||||
})
|
||||
}
|
||||
|
||||
function buildLlmtrProfile(overrides: Partial<ProviderProfile> = {}): ProviderProfile {
|
||||
return buildProfile({
|
||||
provider: 'llmtr',
|
||||
name: 'LLMTR',
|
||||
baseUrl: 'https://llmtr.com/v1',
|
||||
model: 'deepseek/deepseek-v4-flash',
|
||||
apiKey: 'llmtr-test-key',
|
||||
...overrides,
|
||||
})
|
||||
}
|
||||
|
||||
function buildClinePassProfile(overrides: Partial<ProviderProfile> = {}): ProviderProfile {
|
||||
return buildProfile({
|
||||
provider: 'clinepass',
|
||||
@@ -317,6 +330,50 @@ function buildCloudflareProfile(overrides: Partial<ProviderProfile> = {}): Provi
|
||||
}
|
||||
|
||||
describe('applyProviderProfileToProcessEnv', () => {
|
||||
test('LLMTR profile clears an ambient dedicated key so its saved key wins', async () => {
|
||||
const { applyProviderProfileToProcessEnv } =
|
||||
await importFreshProviderProfileModules()
|
||||
process.env.LLMTR_API_KEY = 'ambient-old'
|
||||
|
||||
applyProviderProfileToProcessEnv(
|
||||
buildProfile({
|
||||
provider: 'llmtr',
|
||||
name: 'LLMTR',
|
||||
baseUrl: 'https://llmtr.com/v1',
|
||||
model: 'deepseek/deepseek-v4-flash',
|
||||
apiKey: 'selected-new',
|
||||
}),
|
||||
)
|
||||
|
||||
expect(process.env.LLMTR_API_KEY).toBeUndefined()
|
||||
expect(process.env.OPENAI_API_KEY).toBe('selected-new')
|
||||
expect(
|
||||
resolveRouteCredentialValue({
|
||||
routeId: 'llmtr',
|
||||
baseUrl: process.env.OPENAI_BASE_URL,
|
||||
processEnv: process.env,
|
||||
}),
|
||||
).toBe('selected-new')
|
||||
}, 20_000)
|
||||
|
||||
test('keyless canonical LLMTR profile adopts its ambient dedicated key', async () => {
|
||||
const { applyProviderProfileToProcessEnv } =
|
||||
await importFreshProviderProfileModules()
|
||||
process.env.LLMTR_API_KEY = 'ambient-llmtr-key'
|
||||
|
||||
applyProviderProfileToProcessEnv(buildLlmtrProfile({ apiKey: undefined }))
|
||||
|
||||
expect(process.env.LLMTR_API_KEY).toBe('ambient-llmtr-key')
|
||||
expect(process.env.OPENAI_API_KEY).toBe('ambient-llmtr-key')
|
||||
expect(
|
||||
resolveRouteCredentialValue({
|
||||
routeId: 'llmtr',
|
||||
baseUrl: process.env.OPENAI_BASE_URL,
|
||||
processEnv: process.env,
|
||||
}),
|
||||
).toBe('ambient-llmtr-key')
|
||||
}, 20_000)
|
||||
|
||||
test('applies Azure-style routing from a saved OpenAI-compatible profile', async () => {
|
||||
const { applyProviderProfileToProcessEnv } =
|
||||
await importFreshProviderProfileModules()
|
||||
@@ -1802,6 +1859,87 @@ describe('applyProviderProfileToProcessEnv', () => {
|
||||
expect(process.env.ANTHROPIC_MODEL).toBe('claude-sonnet-4-6')
|
||||
expect(process.env.CLAUDE_CODE_OPENAI_CONTEXT_WINDOWS).toBeUndefined()
|
||||
})
|
||||
|
||||
test('retargeted LLMTR profile withholds its dedicated credential', async () => {
|
||||
const { applyProviderProfileToProcessEnv } =
|
||||
await importFreshProviderProfileModules()
|
||||
|
||||
applyProviderProfileToProcessEnv(
|
||||
buildLlmtrProfile({ baseUrl: 'https://proxy.example/v1' }),
|
||||
)
|
||||
|
||||
expect(process.env.OPENAI_BASE_URL).toBe('https://proxy.example/v1')
|
||||
expect(process.env.OPENAI_API_KEY).toBeUndefined()
|
||||
expect(process.env.LLMTR_API_KEY).toBeUndefined()
|
||||
})
|
||||
|
||||
test('query-bearing LLMTR profiles retain generic proxy capabilities', async () => {
|
||||
const { addProviderProfile, applyProviderProfileToProcessEnv } =
|
||||
await importFreshProviderProfileModules()
|
||||
process.env.LLMTR_API_KEY = 'ambient-llmtr-key'
|
||||
|
||||
const saved = addProviderProfile({
|
||||
provider: 'llmtr',
|
||||
name: 'LLMTR query proxy',
|
||||
baseUrl: 'https://llmtr.com/v1?tenant=proxy',
|
||||
model: 'proxy-model',
|
||||
apiKey: 'saved-llmtr-key',
|
||||
apiFormat: 'responses',
|
||||
authHeader: 'X-Proxy-Key',
|
||||
authScheme: 'raw',
|
||||
authHeaderValue: 'proxy-auth-value',
|
||||
customHeaders: { 'X-Proxy-Trace': 'enabled' },
|
||||
})
|
||||
|
||||
applyProviderProfileToProcessEnv(saved!)
|
||||
|
||||
expect(process.env.OPENAI_API_FORMAT).toBe('responses')
|
||||
expect(process.env.OPENAI_AUTH_HEADER).toBe('X-Proxy-Key')
|
||||
expect(process.env.OPENAI_AUTH_HEADER_VALUE).toBe('proxy-auth-value')
|
||||
expect(process.env.ANTHROPIC_CUSTOM_HEADERS).toBe(
|
||||
'X-Proxy-Trace: enabled',
|
||||
)
|
||||
expect(process.env.OPENAI_API_KEY).toBeUndefined()
|
||||
expect(process.env.LLMTR_API_KEY).toBeUndefined()
|
||||
})
|
||||
|
||||
test('retargeted LLMTR profiles retain generic proxy capabilities', async () => {
|
||||
const { addProviderProfile, applyProviderProfileToProcessEnv } =
|
||||
await importFreshProviderProfileModules()
|
||||
|
||||
const saved = addProviderProfile({
|
||||
provider: 'llmtr',
|
||||
name: 'LLMTR proxy',
|
||||
baseUrl: 'https://proxy.example/v1',
|
||||
model: 'proxy-model',
|
||||
apiKey: 'llmtr-test-key',
|
||||
apiFormat: 'responses',
|
||||
authHeader: 'X-Proxy-Key',
|
||||
authScheme: 'raw',
|
||||
authHeaderValue: 'proxy-auth-value',
|
||||
customHeaders: { 'X-Proxy-Trace': 'enabled' },
|
||||
})
|
||||
|
||||
expect(saved).toMatchObject({
|
||||
apiFormat: 'responses',
|
||||
authHeader: 'X-Proxy-Key',
|
||||
authScheme: 'raw',
|
||||
authHeaderValue: 'proxy-auth-value',
|
||||
customHeaders: { 'X-Proxy-Trace': 'enabled' },
|
||||
})
|
||||
|
||||
applyProviderProfileToProcessEnv(saved!)
|
||||
|
||||
expect(process.env.OPENAI_API_FORMAT).toBe('responses')
|
||||
expect(process.env.OPENAI_AUTH_HEADER).toBe('X-Proxy-Key')
|
||||
expect(process.env.OPENAI_AUTH_SCHEME).toBe('raw')
|
||||
expect(process.env.OPENAI_AUTH_HEADER_VALUE).toBe('proxy-auth-value')
|
||||
expect(process.env.ANTHROPIC_CUSTOM_HEADERS).toBe(
|
||||
'X-Proxy-Trace: enabled',
|
||||
)
|
||||
expect(process.env.OPENAI_API_KEY).toBeUndefined()
|
||||
expect(process.env.LLMTR_API_KEY).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('getProviderProfiles', () => {
|
||||
@@ -1918,6 +2056,7 @@ describe('clearActiveProviderProfile', () => {
|
||||
expect(process.env.OPENAI_BASE_URL).toBeUndefined()
|
||||
expect(process.env.OPENAI_API_KEY).toBeUndefined()
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
describe('Anthropic sentinel survives profile management (#1426)', () => {
|
||||
@@ -3615,6 +3754,66 @@ describe('setActiveProviderProfile', () => {
|
||||
}
|
||||
})
|
||||
|
||||
test('keyed canonical LLMTR profiles persist their saved dedicated credential across restart', async () => {
|
||||
const tempDir = mkdtempSync(join(tmpdir(), 'openclaude-provider-'))
|
||||
const configDir = mkdtempSync(join(tmpdir(), 'openclaude-provider-config-'))
|
||||
process.chdir(tempDir)
|
||||
process.env.CLAUDE_CONFIG_DIR = configDir
|
||||
|
||||
try {
|
||||
const { setActiveProviderProfile } =
|
||||
await importFreshProviderProfileModules()
|
||||
const llmtrProfile = buildLlmtrProfile({ id: 'llmtr_profile' })
|
||||
|
||||
saveMockGlobalConfig(current => ({
|
||||
...current,
|
||||
providerProfiles: [llmtrProfile],
|
||||
}))
|
||||
|
||||
const result = setActiveProviderProfile('llmtr_profile', { configDir })
|
||||
const persisted = JSON.parse(
|
||||
readFileSync(join(configDir, '.openclaude-profile.json'), 'utf8'),
|
||||
)
|
||||
|
||||
expect(result?.id).toBe('llmtr_profile')
|
||||
expect(persisted.profile).toBe('openai')
|
||||
expect(persisted.env).toMatchObject({
|
||||
OPENAI_BASE_URL: 'https://llmtr.com/v1',
|
||||
OPENAI_MODEL: 'deepseek/deepseek-v4-flash',
|
||||
OPENAI_API_KEY: 'llmtr-test-key',
|
||||
LLMTR_API_KEY: 'llmtr-test-key',
|
||||
})
|
||||
|
||||
const { buildStartupEnvFromProfile } = await import(
|
||||
`./providerProfile.js?ts=${Date.now()}-${Math.random()}`
|
||||
)
|
||||
const startupEnv = await buildStartupEnvFromProfile({
|
||||
persisted,
|
||||
processEnv: {
|
||||
LLMTR_API_KEY: 'ambient-unrelated-key',
|
||||
},
|
||||
})
|
||||
|
||||
expect(startupEnv.OPENAI_API_KEY).toBe('llmtr-test-key')
|
||||
expect(startupEnv.LLMTR_API_KEY).toBe('llmtr-test-key')
|
||||
|
||||
delete persisted.env.LLMTR_API_KEY
|
||||
const migratedStartupEnv = await buildStartupEnvFromProfile({
|
||||
persisted,
|
||||
processEnv: {
|
||||
LLMTR_API_KEY: 'ambient-unrelated-key',
|
||||
},
|
||||
})
|
||||
|
||||
expect(migratedStartupEnv.OPENAI_API_KEY).toBe('llmtr-test-key')
|
||||
expect(migratedStartupEnv.LLMTR_API_KEY).toBe('llmtr-test-key')
|
||||
} finally {
|
||||
process.chdir(originalCwd)
|
||||
rmSync(tempDir, { recursive: true, force: true })
|
||||
rmSync(configDir, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
test('persists Xiaomi MiMo profiles using a legacy-compatible openai startup profile', async () => {
|
||||
const tempDir = mkdtempSync(join(tmpdir(), 'openclaude-provider-'))
|
||||
const configDir = mkdtempSync(join(tmpdir(), 'openclaude-provider-config-'))
|
||||
|
||||
@@ -57,6 +57,7 @@ import {
|
||||
isClinePassBaseUrl,
|
||||
isCanonicalApismartInferenceBaseUrl,
|
||||
isCanonicalConcentrateInferenceBaseUrl,
|
||||
isCanonicalLlmtrInferenceBaseUrl,
|
||||
isFireworksBaseUrl,
|
||||
isLongcatBaseUrl,
|
||||
isNearaiBaseUrl,
|
||||
@@ -175,6 +176,15 @@ function isConcentrateProfile(profile: ProviderProfile): boolean {
|
||||
return !baseUrl || isCanonicalConcentrateInferenceBaseUrl(baseUrl)
|
||||
}
|
||||
|
||||
function isLlmtrProfile(profile: ProviderProfile): boolean {
|
||||
const { route } = resolveProfileCompatibility(profile.provider)
|
||||
if (route.routeId !== 'llmtr') {
|
||||
return false
|
||||
}
|
||||
const baseUrl = profile.baseUrl?.trim()
|
||||
return !baseUrl || isCanonicalLlmtrInferenceBaseUrl(baseUrl)
|
||||
}
|
||||
|
||||
function deriveGithubEnterpriseUrl(baseUrl: string | undefined): string | undefined {
|
||||
if (!baseUrl?.trim()) return undefined
|
||||
try {
|
||||
@@ -239,7 +249,7 @@ function normalizeBaseUrl(value: string): string {
|
||||
return trimValue(value).replace(/\/+$/, '')
|
||||
}
|
||||
|
||||
function resolveProfileCapabilityRouteId(
|
||||
export function resolveProfileCapabilityRouteId(
|
||||
provider: string,
|
||||
baseUrl?: string,
|
||||
): string {
|
||||
@@ -259,13 +269,16 @@ function resolveProfileCapabilityRouteId(
|
||||
if (
|
||||
(providerRouteId === 'cloudflare' ||
|
||||
providerRouteId === 'longcat' ||
|
||||
providerRouteId === 'concentrate') &&
|
||||
providerRouteId === 'concentrate' ||
|
||||
providerRouteId === 'llmtr') &&
|
||||
baseUrl &&
|
||||
!(providerRouteId === 'cloudflare'
|
||||
? isCloudflareBaseUrl(baseUrl)
|
||||
: providerRouteId === 'longcat'
|
||||
? isLongcatBaseUrl(baseUrl)
|
||||
: isCanonicalConcentrateInferenceBaseUrl(baseUrl))
|
||||
: providerRouteId === 'concentrate'
|
||||
? isCanonicalConcentrateInferenceBaseUrl(baseUrl)
|
||||
: isCanonicalLlmtrInferenceBaseUrl(baseUrl))
|
||||
) {
|
||||
return 'custom'
|
||||
}
|
||||
@@ -1023,10 +1036,13 @@ export function applyProviderProfileToProcessEnv(
|
||||
route.routeId === 'apismart' && !isApismartProfile(profile)
|
||||
const withholdRetargetedConcentrateCredential =
|
||||
route.routeId === 'concentrate' && !isConcentrateProfile(profile)
|
||||
const withholdRetargetedLlmtrCredential =
|
||||
route.routeId === 'llmtr' && !isLlmtrProfile(profile)
|
||||
if (
|
||||
profile.apiKey &&
|
||||
!withholdRetargetedApismartCredential &&
|
||||
!withholdRetargetedConcentrateCredential
|
||||
!withholdRetargetedConcentrateCredential &&
|
||||
!withholdRetargetedLlmtrCredential
|
||||
) {
|
||||
openAIProfileEnv.OPENAI_API_KEY = profile.apiKey
|
||||
if (route.vendorId === 'minimax' || normalizedProfileBaseUrl.toLowerCase().includes('minimax')) {
|
||||
@@ -1148,6 +1164,22 @@ export function applyProviderProfileToProcessEnv(
|
||||
}
|
||||
}
|
||||
}
|
||||
// A keyless canonical LLMTR profile may use the dedicated credential from
|
||||
// the ambient environment. Profile application clears all managed provider
|
||||
// variables before installing this object, so adopt it here while the
|
||||
// canonical endpoint boundary is still known. Never carry it to a
|
||||
// retargeted profile.
|
||||
if (route.routeId === 'llmtr') {
|
||||
openAIProfileEnv.CLAUDE_CODE_PROVIDER_ROUTE_ID = 'llmtr'
|
||||
if (isLlmtrProfile(profile) && !profile.apiKey) {
|
||||
const ambientLlmtrKey = sanitizeApiKey(process.env.LLMTR_API_KEY)
|
||||
if (ambientLlmtrKey) {
|
||||
openAIProfileEnv.OPENAI_API_KEY =
|
||||
openAIProfileEnv.OPENAI_API_KEY ?? ambientLlmtrKey
|
||||
openAIProfileEnv.LLMTR_API_KEY = ambientLlmtrKey
|
||||
}
|
||||
}
|
||||
}
|
||||
if (route.gatewayId === 'nvidia-nim') {
|
||||
openAIProfileEnv.NVIDIA_NIM = '1'
|
||||
}
|
||||
@@ -1436,15 +1468,19 @@ function buildOpenAICompatibleStartupEnv(
|
||||
const withholdRetargetedConcentrateCredential =
|
||||
activeProfileRouteId === 'concentrate' &&
|
||||
!isConcentrateProfile(activeProfile)
|
||||
const withholdRetargetedLlmtrCredential =
|
||||
activeProfileRouteId === 'llmtr' && !isLlmtrProfile(activeProfile)
|
||||
const isAimlapiProfile =
|
||||
activeProfile.provider === 'aimlapi' ||
|
||||
resolveRouteIdFromBaseUrl(activeProfile.baseUrl) === 'aimlapi'
|
||||
const isConcentrateProfileFlag = isConcentrateProfile(activeProfile)
|
||||
const isLlmtrProfileFlag = isLlmtrProfile(activeProfile)
|
||||
|
||||
if (
|
||||
activeProfile.apiKey &&
|
||||
!withholdRetargetedApismartCredential &&
|
||||
!withholdRetargetedConcentrateCredential
|
||||
!withholdRetargetedConcentrateCredential &&
|
||||
!withholdRetargetedLlmtrCredential
|
||||
) {
|
||||
const strictEnv = buildOpenAIProfileEnv({
|
||||
goal: 'balanced',
|
||||
@@ -1476,6 +1512,9 @@ function buildOpenAICompatibleStartupEnv(
|
||||
if (isConcentrateProfileFlag) {
|
||||
strictEnv.CONCENTRATE_API_KEY = activeProfile.apiKey
|
||||
}
|
||||
if (isLlmtrProfileFlag) {
|
||||
strictEnv.LLMTR_API_KEY = activeProfile.apiKey
|
||||
}
|
||||
if (isClinePassProfile(activeProfile)) {
|
||||
strictEnv.CLINE_API_KEY = activeProfile.apiKey
|
||||
}
|
||||
@@ -1533,10 +1572,14 @@ function buildOpenAICompatibleStartupEnv(
|
||||
if (activeProfileRouteId === 'concentrate') {
|
||||
env.CLAUDE_CODE_PROVIDER_ROUTE_ID = 'concentrate'
|
||||
}
|
||||
if (activeProfileRouteId === 'llmtr') {
|
||||
env.CLAUDE_CODE_PROVIDER_ROUTE_ID = 'llmtr'
|
||||
}
|
||||
if (
|
||||
activeProfile.apiKey &&
|
||||
!withholdRetargetedApismartCredential &&
|
||||
!withholdRetargetedConcentrateCredential
|
||||
!withholdRetargetedConcentrateCredential &&
|
||||
!withholdRetargetedLlmtrCredential
|
||||
) {
|
||||
env.OPENAI_API_KEY = activeProfile.apiKey
|
||||
if (activeProfile.baseUrl?.toLowerCase().includes('bankr')) {
|
||||
@@ -1566,6 +1609,9 @@ function buildOpenAICompatibleStartupEnv(
|
||||
if (isConcentrateProfileFlag) {
|
||||
env.CONCENTRATE_API_KEY = activeProfile.apiKey
|
||||
}
|
||||
if (isLlmtrProfileFlag) {
|
||||
env.LLMTR_API_KEY = activeProfile.apiKey
|
||||
}
|
||||
if (isClinePassProfile(activeProfile)) {
|
||||
env.CLINE_API_KEY = activeProfile.apiKey
|
||||
}
|
||||
@@ -1820,6 +1866,9 @@ function triggerStartupDiscoveryRefreshForProfile(
|
||||
if (route.routeId === 'apismart' && !isApismartProfile(profile)) {
|
||||
return
|
||||
}
|
||||
if (route.routeId === 'llmtr' && !isLlmtrProfile(profile)) {
|
||||
return
|
||||
}
|
||||
|
||||
void refreshStartupDiscoveryForRoute(route.routeId, {
|
||||
baseUrl: profile.baseUrl,
|
||||
|
||||
@@ -20,6 +20,7 @@ describe('clearStartupProviderOverrides', () => {
|
||||
OPENAI_MODEL: 'minimax-m2.7',
|
||||
OPENAI_API_KEYS: 'pool-a,pool-b',
|
||||
OPENAI_API_KEY: 'single-key',
|
||||
LLMTR_API_KEY: 'llmtr-key',
|
||||
MINIMAX_API_KEY: 'sk-minimax',
|
||||
VENICE_API_KEY: 'sk-venice',
|
||||
LONGCAT_API_KEY: 'sk-longcat',
|
||||
@@ -44,6 +45,7 @@ describe('clearStartupProviderOverrides', () => {
|
||||
OPENAI_MODEL: undefined,
|
||||
OPENAI_API_KEYS: undefined,
|
||||
OPENAI_API_KEY: undefined,
|
||||
LLMTR_API_KEY: undefined,
|
||||
MINIMAX_API_KEY: undefined,
|
||||
VENICE_API_KEY: undefined,
|
||||
LONGCAT_API_KEY: undefined,
|
||||
|
||||
@@ -14,6 +14,7 @@ export const STARTUP_PROVIDER_OVERRIDE_ENV_KEYS = [
|
||||
'OPENAI_MODEL',
|
||||
'OPENAI_API_KEYS',
|
||||
'OPENAI_API_KEY',
|
||||
'LLMTR_API_KEY',
|
||||
'OPENAI_ORG',
|
||||
'OPENAI_PROJECT',
|
||||
'OPENAI_ORGANIZATION',
|
||||
|
||||
@@ -32,6 +32,7 @@ const ENV_KEYS = [
|
||||
'MISTRAL_API_KEY',
|
||||
'MINIMAX_API_KEY',
|
||||
'LONGCAT_API_KEY',
|
||||
'LLMTR_API_KEY',
|
||||
'APISMART_API_KEY',
|
||||
'CONCENTRATE_API_KEY',
|
||||
'CONCENTRATE_BASE_URL',
|
||||
@@ -145,6 +146,73 @@ test('openai missing key error includes recovery guidance and config locations',
|
||||
expect(message!).toContain('Saved startup settings can come from')
|
||||
})
|
||||
|
||||
test('LLMTR validation accepts its dedicated credential on the selected route', async () => {
|
||||
process.env.CLAUDE_CODE_USE_OPENAI = '1'
|
||||
process.env.OPENAI_BASE_URL = 'https://llmtr.com/v1'
|
||||
process.env.OPENAI_MODEL = 'deepseek/deepseek-v4-flash'
|
||||
process.env.LLMTR_API_KEY = 'llmtr-key'
|
||||
|
||||
await expect(getProviderValidationError(process.env)).resolves.toBeNull()
|
||||
})
|
||||
|
||||
test('LLMTR validation rejects placeholder dedicated credentials', async () => {
|
||||
process.env.CLAUDE_CODE_USE_OPENAI = '1'
|
||||
process.env.OPENAI_BASE_URL = 'https://llmtr.com/v1'
|
||||
process.env.LLMTR_API_KEY = 'SUA_CHAVE'
|
||||
|
||||
await expect(getProviderValidationError(process.env)).resolves.toBe(
|
||||
'LLMTR auth is required. Set LLMTR_API_KEY or OPENAI_API_KEY.',
|
||||
)
|
||||
})
|
||||
|
||||
test('LLMTR validation rejects an invalid generic fallback credential', async () => {
|
||||
process.env.CLAUDE_CODE_USE_OPENAI = '1'
|
||||
process.env.OPENAI_BASE_URL = 'https://llmtr.com/v1'
|
||||
process.env.OPENAI_API_KEY = 'SUA_CHAVE'
|
||||
|
||||
await expect(getProviderValidationError(process.env)).resolves.toBe(
|
||||
'LLMTR auth is required. Set LLMTR_API_KEY or OPENAI_API_KEY.',
|
||||
)
|
||||
})
|
||||
|
||||
test('LLMTR validation accepts a dedicated credential despite an invalid generic fallback', async () => {
|
||||
process.env.CLAUDE_CODE_USE_OPENAI = '1'
|
||||
process.env.OPENAI_BASE_URL = 'https://llmtr.com/v1'
|
||||
process.env.LLMTR_API_KEY = 'llmtr-key'
|
||||
process.env.OPENAI_API_KEY = 'SUA_CHAVE'
|
||||
|
||||
await expect(getProviderValidationError(process.env)).resolves.toBeNull()
|
||||
})
|
||||
|
||||
test('LLMTR validation falls back from a placeholder dedicated key to OPENAI_API_KEY', async () => {
|
||||
process.env.CLAUDE_CODE_USE_OPENAI = '1'
|
||||
process.env.OPENAI_BASE_URL = 'https://llmtr.com/v1'
|
||||
process.env.OPENAI_MODEL = 'deepseek/deepseek-v4-flash'
|
||||
process.env.LLMTR_API_KEY = 'SUA_CHAVE'
|
||||
process.env.OPENAI_API_KEY = 'llmtr-fallback-key'
|
||||
|
||||
await expect(getProviderValidationError(process.env)).resolves.toBeNull()
|
||||
})
|
||||
|
||||
test('LLMTR validation falls back from a placeholder dedicated key to OPENAI_API_KEYS', async () => {
|
||||
process.env.CLAUDE_CODE_USE_OPENAI = '1'
|
||||
process.env.OPENAI_BASE_URL = 'https://llmtr.com/v1'
|
||||
process.env.OPENAI_MODEL = 'deepseek/deepseek-v4-flash'
|
||||
process.env.LLMTR_API_KEY = 'SUA_CHAVE'
|
||||
process.env.OPENAI_API_KEYS = 'llmtr-pool-key-a,llmtr-pool-key-b'
|
||||
|
||||
await expect(getProviderValidationError(process.env)).resolves.toBeNull()
|
||||
})
|
||||
|
||||
test('LLMTR_API_KEY does not authenticate an unrelated OpenAI-compatible route', async () => {
|
||||
process.env.CLAUDE_CODE_USE_OPENAI = '1'
|
||||
process.env.OPENAI_BASE_URL = 'https://proxy.example/v1'
|
||||
process.env.LLMTR_API_KEY = 'llmtr-key'
|
||||
|
||||
const message = await getProviderValidationError(process.env)
|
||||
expect(message).toContain('OPENAI_API_KEYS or OPENAI_API_KEY is required')
|
||||
})
|
||||
|
||||
test('cloudflare Workers AI URL selects the Cloudflare validation target', async () => {
|
||||
process.env.CLAUDE_CODE_USE_OPENAI = '1'
|
||||
process.env.OPENAI_BASE_URL =
|
||||
|
||||
@@ -142,7 +142,8 @@ function hasUsableCredentialEnvValue(
|
||||
envVar === 'OPENAI_API_KEY' ||
|
||||
envVar === 'AIMLAPI_API_KEY' ||
|
||||
envVar === 'APISMART_API_KEY' ||
|
||||
envVar === 'CONCENTRATE_API_KEY'
|
||||
envVar === 'CONCENTRATE_API_KEY' ||
|
||||
envVar === 'LLMTR_API_KEY'
|
||||
) {
|
||||
return hasUsableOpenAICredential(value)
|
||||
}
|
||||
@@ -344,11 +345,21 @@ function getCredentialEnvValidationError(
|
||||
if (usesOpenAIFallback) {
|
||||
const openAIState = resolveOpenAICredentialEnvState(env)
|
||||
if (openAIState.invalid) {
|
||||
return (
|
||||
validation.invalidCredentialValues?.find(
|
||||
invalidValue => invalidValue.envVar === openAIState.envVar,
|
||||
)?.message ?? null
|
||||
const hasUsableDedicatedCredential = credentialEnvVars.some(
|
||||
envVar =>
|
||||
envVar !== 'OPENAI_API_KEYS' &&
|
||||
envVar !== 'OPENAI_API_KEY' &&
|
||||
hasUsableCredentialEnvValue(env, envVar),
|
||||
)
|
||||
if (!hasUsableDedicatedCredential) {
|
||||
return (
|
||||
validation.invalidCredentialValues?.find(
|
||||
invalidValue => invalidValue.envVar === openAIState.envVar,
|
||||
)?.message ??
|
||||
validation.missingCredentialMessage ??
|
||||
null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -42,6 +42,16 @@ test('buildInheritedEnvVars forwards pooled OpenAI credentials', () => {
|
||||
expect(envVars).toContain('OPENAI_API_KEYS=key-a\\,key-b')
|
||||
})
|
||||
|
||||
test('buildInheritedEnvVars forwards an LLMTR credential without inventing route state', () => {
|
||||
process.env.LLMTR_API_KEY = 'llmtr-key'
|
||||
|
||||
const envVars = buildInheritedEnvVars()
|
||||
|
||||
expect(envVars).toContain('LLMTR_API_KEY=llmtr-key')
|
||||
expect(envVars).not.toContain('CLAUDE_CODE_USE_OPENAI=1')
|
||||
expect(envVars).not.toContain('OPENAI_BASE_URL=')
|
||||
})
|
||||
|
||||
test('buildInheritedEnvVars forwards PATH for source-built teammate tool lookups', () => {
|
||||
process.env.PATH = '/custom/bin:/usr/bin'
|
||||
|
||||
|
||||
@@ -109,6 +109,7 @@ const TEAMMATE_ENV_VARS = [
|
||||
'GH_TOKEN',
|
||||
'OPENAI_API_KEYS',
|
||||
'OPENAI_API_KEY',
|
||||
'LLMTR_API_KEY',
|
||||
'OPENAI_BASE_URL',
|
||||
'OPENAI_MODEL',
|
||||
'GEMINI_API_KEY',
|
||||
|
||||
@@ -140,6 +140,14 @@ export const providers: Provider[] = [
|
||||
envVars: ['OPENROUTER_API_KEY'],
|
||||
notes: 'OpenAI-compatible aggregation across hundreds of hosted models.',
|
||||
},
|
||||
{
|
||||
id: 'llmtr',
|
||||
name: 'LLMTR',
|
||||
group: 'gateways',
|
||||
setup: '/provider or OpenAI-compatible env vars',
|
||||
envVars: ['LLMTR_API_KEY', 'OPENAI_API_KEY'],
|
||||
notes: '/provider and --provider llmtr default to deepseek/deepseek-v4-flash; raw env setup must set OPENAI_BASE_URL=https://llmtr.com/v1 and OPENAI_MODEL. Uses public discovery of tool-capable chat models.',
|
||||
},
|
||||
{
|
||||
id: 'near-ai',
|
||||
name: 'NEAR AI',
|
||||
|
||||
Reference in New Issue
Block a user