Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
7b97272
feat(providers): add LLMTR Turkey-hosted gateway
knowhycodata Jun 14, 2026
a42bf5b
fix(providers): restrict LLMTR to dedicated credentials
knowhycodata Jun 14, 2026
0189ca5
fix(providers): mirror saved LLMTR key into LLMTR_API_KEY
knowhycodata Jun 15, 2026
ae10381
fix(providers): mirror LLMTR key for generic openai profiles + test i…
knowhycodata Jun 15, 2026
42011d5
fix(providers): add LLMTR_API_KEY to shared secret redaction list
knowhycodata Jun 16, 2026
3072385
test(providers): use obviously-fake LLMTR key fixture
knowhycodata Jun 16, 2026
ed8cbcf
test(providers): keep ProviderManager preset order in sync with LLMTR
knowhycodata Jun 16, 2026
7e5cf39
Merge remote-tracking branch 'origin/main' into feat/add-llmtr-provider
knowhycodata Jun 17, 2026
69ee0ca
fix(providers): match LLMTR by hostname, not 'llmtr.com' substring
knowhycodata Jun 17, 2026
be167cc
fix(providers): align isLlmtrBaseUrl with descriptor exact-host routi…
knowhycodata Jun 17, 2026
08c5310
test(providers): assert LLMTR_API_KEY in representative secret keys
knowhycodata Jun 18, 2026
b59862f
Merge remote-tracking branch 'origin/main' into feat/add-llmtr-provider
knowhycodata Jun 18, 2026
8e426e5
Merge branch 'main' into feat/add-llmtr-provider
knowhyco Aug 16, 2026
a428037
feat(providers): broaden LLMTR seed catalog beyond Turkey-hosted models
knowhyco Aug 16, 2026
0540967
fix(providers): scope LLMTR_API_KEY to the canonical LLMTR endpoint
knowhyco Aug 16, 2026
6257b14
fix(providers): finish LLMTR dedicated-credential scoping across runt…
knowhycodata Aug 16, 2026
9e2b41f
feat(providers): recognise LLMTR as an env-only provider
knowhycodata Aug 17, 2026
13a6037
chore: drop probe scratch file committed by mistake
knowhycodata Aug 17, 2026
a921417
fix(providers): gate env-only LLMTR on the canonical endpoint, not th…
knowhycodata Aug 17, 2026
a8d5d05
fix: isolate provider route selection state
knowhycodata Aug 19, 2026
66009c4
fix: address provider route review feedback
knowhycodata Aug 19, 2026
02472d2
fix: validate LLMTR provider credentials
knowhycodata Aug 19, 2026
3c97bd3
fix(providers): resolve LLMTR route identity from provider and base URL
knowhyco Aug 20, 2026
cd8542a
fix(providers): gate startup discovery on the llmtr.com host, not the…
knowhyco Aug 20, 2026
311e5ad
Merge remote-tracking branch 'upstream/main' into feat/add-llmtr-prov…
knowhyco Aug 21, 2026
0ffdd0d
fix(tests): restore CLAUDE_CODE_PROVIDER_ROUTE_ID in the client env f…
knowhyco Aug 21, 2026
3725644
fix(providers): require LLMTR API base path
jatmn Aug 21, 2026
a8e5ff2
fix(providers): reject LLMTR endpoint userinfo
jatmn Aug 21, 2026
b6bdd88
fix(providers): preserve valid selection state
jatmn Aug 21, 2026
0862bff
fix(providers): preserve LLMTR selection and profile credentials
jatmn Aug 21, 2026
7a1d1f4
fix(profiles): retain proxy capabilities for LLMTR
jatmn Aug 21, 2026
b4d2b08
fix(swarm): propagate route-owned teammate credentials
jatmn Aug 21, 2026
f1161ee
fix(llmtr): constrain discovery and credential routes
jatmn Aug 21, 2026
b8b28f3
fix(llmtr): align transport and inherited routing
jatmn Aug 21, 2026
1c61a62
test(llmtr): enforce fixed auth and teammate profiles
jatmn Aug 21, 2026
c9b3309
fix(swarm): inherit complete provider transport state
jatmn Aug 21, 2026
34e7784
fix(routing): honor explicit env-only provider intent
jatmn Aug 21, 2026
b0e8911
fix(provider): preserve explicit validation intent
jatmn Aug 21, 2026
c172f40
fix(validation): align key-only intent precedence
jatmn Aug 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,16 @@ ANTHROPIC_API_KEY=sk-ant-your-key-here
# OPENAI_BASE_URL=https://api.aimlapi.com/v1
# OPENAI_MODEL=gpt-4o

# For LLMTR (OpenAI-compatible multi-vendor gateway, hosted in Turkey), get a
# key at https://llmtr.com, use the OpenAI-compatible route flag above and set:
# LLMTR_API_KEY=your-llmtr-key-here
# OPENAI_BASE_URL=https://llmtr.com/v1
# OPENAI_MODEL=anthropic/claude-sonnet-4.6
# Other routes: openai/gpt-5.4, google/gemini-3.1-pro-preview,
# deepseek/deepseek-v4-pro, zai/glm-5.2, minimax/minimax-m3, qwen/qwen3.7-max
# Turkey-hosted tool-capable models: llmtr/gemma-4,
# llmtr/muse-glimmer-30b-tr

# 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).
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,7 @@ Advanced and source-build guides:
| 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 |
| 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 |
| LLMTR | `/provider` or OpenAI-compatible env vars | OpenAI-compatible multi-vendor gateway at `https://llmtr.com/v1`; uses `LLMTR_API_KEY` and routes by `OPENAI_MODEL`. Its own `llmtr/*` models are served from Turkey; the passthrough routes (OpenAI/Anthropic/Google/Qwen/DeepSeek/Z.ai/MiniMax/Moonshot/Mistral) forward request content to the selected upstream provider, under that provider's processing terms and data location |
| Fireworks AI | `/provider` or env vars | First-class provider with 276 curated models (DeepSeek, Qwen, Llama, Gemma, and more); uses `FIREWORKS_API_KEY` |
| LongCat | `/provider` or env vars | Meituan LongCat OpenAI-compatible API at `https://api.longcat.chat/openai/v1`; uses `LONGCAT_API_KEY` and defaults to `LongCat-2.0` |
| ClinePass | `/provider` or env vars | AI model gateway with usage limits (5hr, weekly, monthly); uses `CLINE_API_KEY` at `https://api.cline.bot/api/v1` |
Expand Down
1 change: 1 addition & 0 deletions src/components/ProviderManager.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ const PRESET_ORDER = [
'Google AI / Gemini',
'Groq',
'Hicap',
'LLMTR',
'LM Studio',
'Atomic Chat',
'Ollama',
Expand Down
1 change: 1 addition & 0 deletions src/integrations/compatibility.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
'together',
'groq',
'hicap',
'llmtr',
'azure-openai',
'openrouter',
'lmstudio',
Expand Down Expand Up @@ -59,7 +60,7 @@

describe('compatibility mappings', () => {
test('cover every current provider preset exactly once', () => {
expect(PRESET_VENDOR_MAP.map(mapping => mapping.preset).sort()).toEqual(

Check failure on line 63 in src/integrations/compatibility.test.ts

View workflow job for this annotation

GitHub Actions / smoke-and-tests (24.11.x)

error: expect(received).toEqual(expected)

[ "aimlapi", "anthropic", "apismart", "atlas-cloud", "atomic-chat", "azure-openai", "bankr", "clinepass", "cloudflare", + "concentrate", "custom", "custom-anthropic", "dashscope-cn", "dashscope-intl", "deepseek", "fireworks", "gemini", "gitlawb-opengateway", "groq", "hicap", "kimi-code", "llmtr", "lmstudio", "longcat", "minimax", "mistral", "moonshotai", "nearai", "nvidia-nim", "ollama", "openai", "opencode", "opencode-go", "openrouter", "together", "venice", "xai", "xiaomi-mimo", "xiaomi-mimo-token", "zai", ] - Expected - 0 + Received + 1 at <anonymous> (/home/runner/work/openclaude/openclaude/src/integrations/compatibility.test.ts:63:69)

Check failure on line 63 in src/integrations/compatibility.test.ts

View workflow job for this annotation

GitHub Actions / smoke-and-tests (22)

error: expect(received).toEqual(expected)

[ "aimlapi", "anthropic", "apismart", "atlas-cloud", "atomic-chat", "azure-openai", "bankr", "clinepass", "cloudflare", + "concentrate", "custom", "custom-anthropic", "dashscope-cn", "dashscope-intl", "deepseek", "fireworks", "gemini", "gitlawb-opengateway", "groq", "hicap", "kimi-code", "llmtr", "lmstudio", "longcat", "minimax", "mistral", "moonshotai", "nearai", "nvidia-nim", "ollama", "openai", "opencode", "opencode-go", "openrouter", "together", "venice", "xai", "xiaomi-mimo", "xiaomi-mimo-token", "zai", ] - Expected - 0 + Received + 1 at <anonymous> (/home/runner/work/openclaude/openclaude/src/integrations/compatibility.test.ts:63:69)
[...EXPECTED_PRESETS].sort(),
)
expect(new Set(PRESET_VENDOR_MAP.map(mapping => mapping.preset)).size).toBe(
Expand Down
148 changes: 148 additions & 0 deletions src/integrations/discoveryService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const originalEnv = {
OPENAI_API_KEYS: process.env.OPENAI_API_KEYS,
OPENAI_MODEL: process.env.OPENAI_MODEL,
APISMART_API_KEY: process.env.APISMART_API_KEY,
LLMTR_API_KEY: process.env.LLMTR_API_KEY,
ANTHROPIC_CUSTOM_HEADERS: process.env.ANTHROPIC_CUSTOM_HEADERS,
CLAUDE_CODE_USE_OPENAI: process.env.CLAUDE_CODE_USE_OPENAI,
CLAUDE_CODE_USE_GEMINI: process.env.CLAUDE_CODE_USE_GEMINI,
Expand Down Expand Up @@ -63,6 +64,7 @@ function clearProviderEnv(): void {
delete process.env.OPENAI_API_KEYS
delete process.env.OPENAI_MODEL
delete process.env.APISMART_API_KEY
delete process.env.LLMTR_API_KEY
delete process.env.ANTHROPIC_CUSTOM_HEADERS
delete process.env.CLAUDE_CODE_USE_OPENAI
delete process.env.CLAUDE_CODE_USE_GEMINI
Expand Down Expand Up @@ -100,6 +102,7 @@ afterEach(() => {
restoreEnvValue('OPENAI_API_KEYS')
restoreEnvValue('OPENAI_MODEL')
restoreEnvValue('APISMART_API_KEY')
restoreEnvValue('LLMTR_API_KEY')
restoreEnvValue('ANTHROPIC_CUSTOM_HEADERS')
restoreEnvValue('CLAUDE_CODE_USE_OPENAI')
restoreEnvValue('CLAUDE_CODE_USE_GEMINI')
Expand Down Expand Up @@ -141,6 +144,151 @@ describe('discoverModelsForRoute', () => {
expect(authorization).not.toBe('Bearer apismart-secret')
})

test('does not send an LLMTR key to a non-canonical discovery URL', async () => {
const { discoverModelsForRoute } = await loadDiscoveryServiceModule()
process.env.LLMTR_API_KEY = 'llmtr-secret'
// Plaintext and non-default port share the llmtr.com hostname, so a
// host-scoped check would accept both. A proxy host fails the host check
// but reaches this path through the saved route id.
for (const baseUrl of [
'http://llmtr.com/v1',
'https://llmtr.com:8443/v1',
'https://proxy.example/v1',
]) {
let didFetch = false
let authorization: string | null | undefined
setMockFetch(mock((_input: string | URL | Request, init?: RequestInit) => {
didFetch = true
authorization = new Headers(init?.headers).get('authorization')
return Promise.resolve(
new Response(JSON.stringify({ data: [] }), {
headers: { 'Content-Type': 'application/json' },
}),
)
}) as unknown as typeof globalThis.fetch)

await discoverModelsForRoute('llmtr', { baseUrl, forceRefresh: true })

expect(didFetch).toBe(true)
expect(authorization).not.toBe('Bearer llmtr-secret')
}
})

test('does not send a retargeted LLMTR profile key to a non-canonical discovery URL', async () => {
const { discoverModelsForRoute } = await loadDiscoveryServiceModule()
// The profile's own key is supplied by the caller, ahead of the ambient
// lookup. Without a canonical gate in front of it, a retargeted profile
// would still authenticate discovery against the endpoint that
// applyProviderProfileToProcessEnv already withheld the credential from.
let didFetch = false
let authorization: string | null | undefined
setMockFetch(mock((_input: string | URL | Request, init?: RequestInit) => {
didFetch = true
authorization = new Headers(init?.headers).get('authorization')
return Promise.resolve(
new Response(JSON.stringify({ data: [] }), {
headers: { 'Content-Type': 'application/json' },
}),
)
}) as unknown as typeof globalThis.fetch)

await discoverModelsForRoute('llmtr', {
baseUrl: 'https://proxy.example/v1',
apiKey: 'llmtr-profile-secret',
forceRefresh: true,
})

expect(didFetch).toBe(true)
expect(authorization).not.toBe('Bearer llmtr-profile-secret')
})

test('does not send credentials to LLMTR public discovery', async () => {
const { discoverModelsForRoute } = await loadDiscoveryServiceModule()
process.env.LLMTR_API_KEY = 'llmtr-secret'
let authorization: string | null | undefined
setMockFetch(mock((_input: string | URL | Request, init?: RequestInit) => {
authorization = new Headers(init?.headers).get('authorization')
return Promise.resolve(
new Response(JSON.stringify({ data: [] }), {
headers: { 'Content-Type': 'application/json' },
}),
)
}) as unknown as typeof globalThis.fetch)

await discoverModelsForRoute('llmtr', {
baseUrl: 'https://llmtr.com/v1',
forceRefresh: true,
})

expect(authorization).toBeNull()
})

test('LLMTR discovery exposes only tool-capable chat-completions models', async () => {
const { discoverModelsForRoute } = await loadDiscoveryServiceModule()
setMockFetch(mock(() =>
Promise.resolve(
new Response(
JSON.stringify({
data: [
{
id: 'vendor/agent-model',
name: 'Agent Model',
supported_operations: ['CHAT_COMPLETIONS'],
supported_endpoints: ['/v1/chat/completions'],
supported_parameters: ['tools', 'reasoning_effort'],
context_length: 131072,
top_provider: { max_completion_tokens: 32768 },
architecture: { input_modalities: ['text', 'image'] },
},
{
id: 'openai/responses-only',
supported_operations: ['RESPONSES'],
supported_endpoints: ['/v1/responses'],
supported_parameters: ['tools'],
},
{
id: 'vendor/chat-without-tools',
supported_operations: ['CHAT_COMPLETIONS'],
supported_endpoints: ['/v1/chat/completions'],
supported_parameters: ['temperature'],
},
{
id: 'vendor/embedding-model',
supported_operations: ['EMBEDDINGS'],
supported_endpoints: ['/v1/embeddings'],
supported_parameters: [],
},
],
}),
{ headers: { 'Content-Type': 'application/json' } },
),
)
) as unknown as typeof globalThis.fetch)

const result = await discoverModelsForRoute('llmtr', {
baseUrl: 'https://llmtr.com/v1',
apiKey: 'llmtr-secret',
forceRefresh: true,
})

const discovered = result?.models.find(
model => model.apiName === 'vendor/agent-model',
)
expect(discovered).toMatchObject({
label: 'Agent Model',
contextWindow: 131072,
maxOutputTokens: 32768,
capabilities: {
supportsFunctionCalling: true,
supportsVision: true,
supportsReasoning: true,
},
})
expect(result?.models.some(model => model.apiName === 'openai/responses-only')).toBe(false)
expect(result?.models.some(model => model.apiName === 'vendor/chat-without-tools')).toBe(false)
expect(result?.models.some(model => model.apiName === 'vendor/embedding-model')).toBe(false)
})

test('uses built-in openai-compatible discovery and caches results for dynamic routes', async () => {
const { discoverModelsForRoute } = await loadDiscoveryServiceModule()

Expand Down
10 changes: 10 additions & 0 deletions src/integrations/discoveryService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { resolveRouteIdFromBaseUrl } from './index.js'
import {
getRouteDescriptor,
isCanonicalApismartInferenceBaseUrl,
isCanonicalLlmtrInferenceBaseUrl,
isCanonicalXaiInferenceBaseUrl,
resolveActiveRouteIdFromEnv,
resolveRouteCredentialValue,
Expand Down Expand Up @@ -188,6 +189,15 @@ function getRouteDiscoveryApiKey(
) {
return undefined
}
// LLMTR is dedicatedCredentialsOnly, so the same boundary applies. This has
// to sit ahead of the caller-supplied key as well, not only ahead of the
// ambient lookup: a profile retargeted to `http://llmtr.com`, to a non-default
// port, or to a proxy still carries its own `apiKey`, and returning it here
// would authenticate discovery against the endpoint from which
// applyProviderProfileToProcessEnv already withheld the credential.
if (routeId === 'llmtr' && !isCanonicalLlmtrInferenceBaseUrl(baseUrl)) {
return undefined
}

if (hasInvalidCredentialPlaceholder(options?.apiKey)) {
return undefined
Expand Down
Loading
Loading