Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 7 additions & 0 deletions src/integrations/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,13 @@ describe('loaded registry validation', () => {
expect(routeSupportsCustomHeaders('custom-anthropic')).toBe(true)
})

test('local gateways support auth headers without API-format selection', () => {
for (const routeId of ['ollama', 'lmstudio']) {
expect(routeSupportsApiFormatSelection(routeId)).toBe(false)
expect(routeSupportsAuthHeaders(routeId)).toBe(true)
}
})

test('route catalogs do not duplicate defaultModel with catalog default flags', () => {
const routes = [...getAllVendors(), ...getAllGateways()]
expect(
Expand Down
1 change: 1 addition & 0 deletions src/integrations/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ export {
isCloudflareBaseUrl,
normalizeXiaomiMimoBaseUrl,
resolveActiveRouteIdFromEnv,
resolveLocalCompatibleRouteIdFromBaseUrl,
resolveRouteIdFromBaseUrl,
routeSupportsApiFormatSelection,
routeSupportsAuthHeaders,
Expand Down
102 changes: 102 additions & 0 deletions src/integrations/routeMetadata.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
isCloudflareBaseUrl,
resolveActiveRouteIdFromEnv,
resolveRouteCredentialValue,
resolveLocalCompatibleRouteIdFromBaseUrl,
resolveRouteIdFromBaseUrl,
} from './routeMetadata.js'

Expand Down Expand Up @@ -88,6 +89,107 @@ test('getRouteProviderTypeLabel falls back safely for unknown routes', () => {
)
})

test('resolveRouteIdFromBaseUrl only treats loopback local ports as known local routes', () => {
expect(resolveRouteIdFromBaseUrl('http://localhost:11434/v1')).toBe('ollama')
expect(resolveRouteIdFromBaseUrl('http://127.0.0.1:11434/v1')).toBe(
'ollama',
)
expect(resolveRouteIdFromBaseUrl('http://[::1]:11434/v1')).toBe('ollama')
expect(resolveRouteIdFromBaseUrl('http://localhost:1234/v1')).toBe(
'lmstudio',
)
expect(resolveRouteIdFromBaseUrl('http://127.0.0.1:1234/v1')).toBe(
'lmstudio',
)
expect(resolveRouteIdFromBaseUrl('http://[::1]:1234/v1')).toBe('lmstudio')
})

test('resolveRouteIdFromBaseUrl preserves custom URLs that resemble local routes', () => {
expect(resolveRouteIdFromBaseUrl('https://proxy.example.com:11434/v1')).toBe(
null,
)
expect(resolveRouteIdFromBaseUrl('https://proxy.example.com:1234/v1')).toBe(
null,
)
expect(resolveRouteIdFromBaseUrl('https://myollama.example.com/v1')).toBe(
null,
)
expect(resolveRouteIdFromBaseUrl('https://lmstudio.example.com/v1')).toBe(
null,
)
expect(resolveRouteIdFromBaseUrl('https://example.com/ollama/v1')).toBe(null)
expect(resolveRouteIdFromBaseUrl('https://example.com/lm-studio/v1')).toBe(
null,
)
expect(resolveRouteIdFromBaseUrl('https://localhost:11434/v1')).toBe(null)
})

test('remote Ollama-compatible URLs keep Ollama route identity for runtime decisions', () => {
// Tightened from the pre-PR broad matcher: only a host whose dot-label is
// exactly `ollama` (e.g. `ollama.corp.example.com`) keeps the Ollama route
// for runtime decisions. A :11434 port on a non-loopback host no longer
// classifies as Ollama — it would otherwise silently route vLLM / LM Studio
// tunnels through the local-shim catalog and drop responses-API-format
// selection. Likewise `my-ollama-server` (label `my-ollama-server`) and
// `myollama` (no `ollama` dot-label) no longer match.
expect(
resolveLocalCompatibleRouteIdFromBaseUrl(
'https://ollama.corp.example.com/v1',
),
).toBe('ollama')
expect(
resolveLocalCompatibleRouteIdFromBaseUrl('https://ollama.example.com/v1'),
).toBe('ollama')
expect(
resolveLocalCompatibleRouteIdFromBaseUrl('http://203.0.113.5:11434/v1'),
).toBe(null)
expect(
resolveLocalCompatibleRouteIdFromBaseUrl(
'http://my-ollama-server.example.com:11434/v1',
),
).toBe(null)
expect(
resolveLocalCompatibleRouteIdFromBaseUrl(
'https://my-ollama.example.com/v1',
),
).toBe(null)
expect(
resolveLocalCompatibleRouteIdFromBaseUrl('https://myollama.example.com/v1'),
).toBe(null)
expect(
resolveLocalCompatibleRouteIdFromBaseUrl(
'https://ollama-corp.example.com/v1',
),
).toBe(null)
expect(
resolveLocalCompatibleRouteIdFromBaseUrl(
'https://proxy.example.com:11434/v1',
),
).toBe(null)

expect(
resolveActiveRouteIdFromEnv({
CLAUDE_CODE_USE_OPENAI: '1',
OPENAI_BASE_URL: 'https://ollama.corp.example.com/v1',
}),
).toBe('ollama')
})

test('xai --provider replaces a stale remote Ollama URL identified by hostname token', () => {
// Loopback-port cross-provider replacement is exercised by the
// `${provider} replaces a stale known provider base URL` test in
// providerFlag.test.ts (the loopback :11434 path resolves to the `ollama`
// route via the strict resolver). This companion covers the remote
// hostname-token form kept by resolveLocalCompatibleRouteIdFromBaseUrl: a
// `ollama.corp.example.com` URL must also be replaced on provider switch,
// confirming MEDIUM-2's resolver alignment (LOW-5).
expect(
resolveLocalCompatibleRouteIdFromBaseUrl(
'https://ollama.corp.example.com/v1',
),
).toBe('ollama')
})

test('getRouteCredentialEnvVars keeps descriptor env vars and openai fallback for openai-compatible routes', () => {
expect(getRouteCredentialEnvVars('custom')).toEqual([
'OPENAI_API_KEYS',
Expand Down
92 changes: 79 additions & 13 deletions src/integrations/routeMetadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,26 +99,38 @@ function getAllRoutes(): RouteDescriptor[] {
return [...getAllGateways(), ...getAllVendors(), ...getAllAnthropicProxies()]
}

// Route inference is intentionally narrower than local-network detection: only
// built-in local defaults are known enough to replace as stale provider URLs.
function isDefaultLocalRouteHostname(hostname: string): boolean {
const normalizedHostname =
hostname.startsWith('[') && hostname.endsWith(']')
? hostname.slice(1, -1)
: hostname

return (
normalizedHostname === 'localhost' ||
normalizedHostname === '127.0.0.1' ||
normalizedHostname === '::1'
)
}
Comment thread
GautamBytes marked this conversation as resolved.

function resolveKnownLocalRouteIdFromBaseUrl(baseUrl?: string): string | null {
if (!baseUrl) {
return null
}

try {
const parsed = new URL(baseUrl)
const host = parsed.host.toLowerCase()
const hostname = parsed.hostname.toLowerCase()
const path = parsed.pathname.toLowerCase()
const haystack = `${hostname} ${path}`

if (host.endsWith(':11434') || haystack.includes('ollama')) {
if (parsed.protocol !== 'http:' || !isDefaultLocalRouteHostname(hostname)) {
return null
}

if (parsed.port === '11434') {
return 'ollama'
}
if (
host.endsWith(':1234') ||
haystack.includes('lmstudio') ||
haystack.includes('lm-studio')
) {
if (parsed.port === '1234') {
return 'lmstudio'
}
} catch {
Expand All @@ -128,6 +140,51 @@ function resolveKnownLocalRouteIdFromBaseUrl(baseUrl?: string): string | null {
return null
}

// Match a single dot-label exactly, NOT a dash-split sub-token. Splitting a
// label like `ollama-corp` on `[-_]+` would `includes('ollama')` it as a
// sub-token and misclassify any third-party host whose label merely embeds
// `ollama`. Exact dot-label match keeps `ollama` and `ollama.example.com`
// matching while excluding `ollama-corp.example.com` / `my-ollama.example.com`.
function hostnameContainsRouteToken(hostname: string, token: string): boolean {
return hostname.split('.').some(label => label === token)
}

function resolveRemoteOllamaRouteIdFromBaseUrl(baseUrl?: string): string | null {
if (!baseUrl) {
return null
}

try {
const parsed = new URL(baseUrl)
const hostname = parsed.hostname.toLowerCase()

if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
return null
}

// No host-agnostic :11434 rule: a reverse proxy / vLLM / LM Studio tunnel
// bound to 11434 from a remote host would silently inherit Ollama's
// local-shim catalog + responses-API-format selection. Loopback :11434 is
// already handled by the strict resolver (resolveKnownLocalRouteIdFromBaseUrl).
if (hostnameContainsRouteToken(hostname, 'ollama')) {
return 'ollama'
}
} catch {
return null
}

return null
}

export function resolveLocalCompatibleRouteIdFromBaseUrl(
baseUrl?: string,
): string | null {
return (
resolveRouteIdFromBaseUrl(baseUrl) ??
resolveRemoteOllamaRouteIdFromBaseUrl(baseUrl)
)
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

export function getRouteDescriptor(
routeId: string,
): RouteDescriptor | null {
Expand Down Expand Up @@ -879,7 +936,16 @@ function routeSupportsOpenAIShimOption(
option: 'supportsApiFormatSelection' | 'supportsAuthHeaders',
): boolean {
const descriptor = getRouteDescriptor(routeId)
if (!descriptor || descriptor.transportConfig.kind !== 'openai-compatible') {
if (!descriptor) {
return false
}

const transportKind = descriptor.transportConfig.kind
const supportsOptionForTransport =
transportKind === 'openai-compatible' ||
(option === 'supportsAuthHeaders' && transportKind === 'local')

if (!supportsOptionForTransport) {
return false
}

Expand Down Expand Up @@ -1024,7 +1090,7 @@ export function resolveActiveRouteIdFromEnv(
if (isEnvTruthy(processEnv.CLAUDE_CODE_USE_OPENAI)) {
const baseUrl =
processEnv.OPENAI_BASE_URL ?? processEnv.OPENAI_API_BASE
const matchedRoute = resolveRouteIdFromBaseUrl(baseUrl)
const matchedRoute = resolveLocalCompatibleRouteIdFromBaseUrl(baseUrl)

if (matchedRoute) {
return matchedRoute
Expand All @@ -1045,7 +1111,7 @@ export function resolveActiveRouteIdFromEnv(
}
// A custom/unknown profile may still target a known gateway via its
// saved base URL; prefer that route over the generic openai/custom path.
const profileBaseUrlRoute = resolveRouteIdFromBaseUrl(
const profileBaseUrlRoute = resolveLocalCompatibleRouteIdFromBaseUrl(
options.activeProfileBaseUrl,
)
if (profileBaseUrlRoute) {
Expand Down Expand Up @@ -1080,7 +1146,7 @@ export function resolveActiveRouteIdFromEnv(
}
// A custom/unknown profile may still target a known gateway via its
// saved base URL; prefer that route over the generic anthropic fallback.
const profileBaseUrlRoute = resolveRouteIdFromBaseUrl(
const profileBaseUrlRoute = resolveLocalCompatibleRouteIdFromBaseUrl(
options.activeProfileBaseUrl,
)
if (profileBaseUrlRoute) {
Expand Down
5 changes: 4 additions & 1 deletion src/integrations/runtimeMetadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
getRouteDescriptor,
resolveRouteCredentialValue,
resolveActiveRouteIdFromEnv,
resolveLocalCompatibleRouteIdFromBaseUrl,
resolveRouteIdFromBaseUrl,
type RouteDescriptor,
} from './routeMetadata.js'
Expand Down Expand Up @@ -269,7 +270,9 @@ export function resolveOpenAIShimRuntimeContext(options?: {
activeProfileProvider: options?.activeProfileProvider,
activeProfileBaseUrl: options?.baseUrl,
})
const baseUrlRouteId = resolveRouteIdFromBaseUrl(options?.baseUrl)
const baseUrlRouteId = resolveLocalCompatibleRouteIdFromBaseUrl(
options?.baseUrl,
)
const routeId =
options?.preferBaseUrlRoute && options.baseUrl !== undefined
? baseUrlRouteId
Expand Down
8 changes: 5 additions & 3 deletions src/services/api/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,6 @@ const realProviders = {
}
mock.module('../../utils/model/providers.js', () => realProviders)
mock.module('src/utils/model/providers.js', () => realProviders)
const { getAnthropicClient } = await import(
`./client.js?real=${Date.now()}-${Math.random()}`,
)

type FetchType = typeof globalThis.fetch

Expand All @@ -38,6 +35,8 @@ type ShimClient = {

const originalFetch = globalThis.fetch
const originalMacro = (globalThis as Record<string, unknown>).MACRO
let clientImportCounter = 0
let getAnthropicClient: typeof import('./client.js').getAnthropicClient
const originalEnv = {
CLAUDE_CODE_USE_OPENAI: process.env.CLAUDE_CODE_USE_OPENAI,
CLAUDE_CODE_USE_BEDROCK: process.env.CLAUDE_CODE_USE_BEDROCK,
Expand Down Expand Up @@ -131,6 +130,9 @@ function clearEnvForMiniMaxOnlyTest(): void {

beforeEach(async () => {
await acquireSharedMutationLock('client.test.ts')
;({ getAnthropicClient } = await import(
`./client.js?client-test-${clientImportCounter++}`
))
;(globalThis as Record<string, unknown>).MACRO = { VERSION: 'test-version' }
process.env.CLAUDE_CODE_USE_GEMINI = '1'
process.env.GEMINI_API_KEY = 'gemini-test-key'
Expand Down
8 changes: 6 additions & 2 deletions src/services/api/openaiShim.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6984,7 +6984,11 @@ test('uses native Ollama chat endpoint when local base URL omits /v1', async ()
})

test('keeps remote Ollama-named gateways on chat completions', async () => {
process.env.OPENAI_BASE_URL = 'https://ollama-gateway.example.com/v1'
// Tightened remote-Ollama classifier: only a host whose dot-label is exactly
// `ollama` (e.g. `ollama.example.com`) inherits the Ollama shim's chat-only
// transport. Hosts whose label merely embeds `ollama` after a dash (e.g.
// `ollama-gateway`) no longer match the Ollama route.
process.env.OPENAI_BASE_URL = 'https://ollama.example.com/v1'

const requestUrls: string[] = []
globalThis.fetch = (async (input, init) => {
Expand All @@ -7009,7 +7013,7 @@ test('keeps remote Ollama-named gateways on chat completions', async () => {
).resolves.toBeDefined()

expect(requestUrls).toEqual([
'https://ollama-gateway.example.com/v1/chat/completions',
'https://ollama.example.com/v1/chat/completions',
])
})

Expand Down
Loading