Skip to content
Merged
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
18 changes: 17 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -380,7 +380,23 @@ ANTHROPIC_API_KEY=sk-ant-your-key-here


# -----------------------------------------------------------------------------
# Option 13: ClinePass
# Option 13: LongCat
# -----------------------------------------------------------------------------
# LongCat is Meituan's OpenAI-compatible API for LongCat-2.0.
# Get your API key from https://longcat.chat/platform/api_keys
# Docs: https://longcat.chat/platform/docs/
#
# CLAUDE_CODE_USE_OPENAI=1
# LONGCAT_API_KEY=your-longcat-key-here
# OPENAI_BASE_URL=https://api.longcat.chat/openai/v1
# OPENAI_MODEL=LongCat-2.0
#
# Or run: openclaude --provider longcat
# Or configure interactively with /provider


Comment thread
jatmn marked this conversation as resolved.
# -----------------------------------------------------------------------------
# Option 14: ClinePass
# -----------------------------------------------------------------------------
# ClinePass is an AI model gateway with usage limits (5hr, weekly, monthly).
# Get your API key from https://app.cline.bot/dashboard/subscription?personal=true
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,7 @@ Advanced and source-build guides:
| 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 |
| 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` |
| 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` |
| Gemini | `/provider` or env vars | Supports API key only |
| GitHub Models | `/onboard-github` | Interactive onboarding with saved credentials |
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 @@ -142,6 +142,7 @@ const PRESET_ORDER = [
'LM Studio',
'Atomic Chat',
'Ollama',
'LongCat',
'MiniMax',
'Mistral AI',
'Moonshot AI - API',
Expand Down
15 changes: 15 additions & 0 deletions src/integrations/brands/longcat.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { defineBrand } from '../define.js'

export default defineBrand({
id: 'longcat',
label: 'LongCat',
canonicalVendorId: 'longcat',
defaultCapabilities: {
supportsStreaming: true,
supportsFunctionCalling: false,
supportsJsonMode: false,
supportsReasoning: true,
supportsPreciseTokenCount: false,
},
modelIds: ['LongCat-2.0'],
})
1 change: 1 addition & 0 deletions src/integrations/compatibility.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ const EXPECTED_PRESETS = [
'gitlawb-opengateway',
'nearai',
'fireworks',
'longcat',
'opencode',
'opencode-go',
'clinepass',
Expand Down
2 changes: 2 additions & 0 deletions src/integrations/descriptors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ export interface OpenAIShimTransportConfig {
enableToolStreaming?: boolean
maxTokensField?: OpenAIShimTokenField
removeBodyFields?: string[]
/** Whether the endpoint accepts image content parts. Defaults to true. */
supportsImageInputs?: boolean
/** Override the endpoint path for this model (e.g., '/responses', '/messages'). */
endpointPath?: string
}
Expand Down
9 changes: 6 additions & 3 deletions src/integrations/generated/integrationArtifacts.generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import vendorBankr from '../vendors/bankr.js'
import vendorDeepseek from '../vendors/deepseek.js'
import vendorFireworks from '../vendors/fireworks.js'
import vendorGemini from '../vendors/gemini.js'
import vendorLongcat from '../vendors/longcat.js'
import vendorMinimax from '../vendors/minimax.js'
import vendorMoonshot from '../vendors/moonshot.js'
import vendorNearai from '../vendors/nearai.js'
Expand Down Expand Up @@ -53,6 +54,7 @@ import brandGlm from '../brands/glm.js'
import brandGpt from '../brands/gpt.js'
import brandKimi from '../brands/kimi.js'
import brandLlama from '../brands/llama.js'
import brandLongcat from '../brands/longcat.js'
import brandMinimax from '../brands/minimax.js'
import brandMistral from '../brands/mistral.js'
import brandNearai from '../brands/nearai.js'
Expand All @@ -70,6 +72,7 @@ import modelGlm from '../models/glm.js'
import modelGpt from '../models/gpt.js'
import modelKimi from '../models/kimi.js'
import modelLlama from '../models/llama.js'
import modelLongcat from '../models/longcat.js'
import modelMinimax from '../models/minimax.js'
import modelMistral from '../models/mistral.js'
import modelNearai from '../models/nearai.js'
Expand All @@ -81,11 +84,11 @@ import modelTencent from '../models/tencent.js'
import modelXai from '../models/xai.js'
import modelXiaomiMimo from '../models/xiaomi-mimo.js'

export const VENDOR_DESCRIPTORS = [vendorAnthropic, vendorBankr, vendorDeepseek, vendorFireworks, vendorGemini, vendorMinimax, vendorMoonshot, vendorNearai, vendorOpenai, vendorVenice, vendorXai, vendorXiaomiMimo, vendorZai] as const satisfies readonly VendorDescriptor[]
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, gatewayAtlasCloud, gatewayAtomicChat, gatewayAzureOpenai, gatewayBedrock, gatewayClinepass, gatewayCloudflare, 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 ANTHROPIC_PROXY_DESCRIPTORS = [anthropicproxyCustom] as const satisfies readonly AnthropicProxyDescriptor[]
export const BRAND_DESCRIPTORS = [brandClaude, brandDeepseek, brandFireworks, brandGemini, brandGlm, brandGpt, brandKimi, brandLlama, 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, modelLlama, modelMinimax, modelMistral, modelNearai, modelNemotron, modelOpenaiCompatibleAlias, modelOpencode, modelQwen, modelTencent, modelXai, modelXiaomiMimo] as const satisfies readonly (readonly ModelDescriptor[])[]
export const BRAND_DESCRIPTORS = [brandClaude, brandDeepseek, brandFireworks, brandGemini, brandGlm, brandGpt, brandKimi, brandLlama, brandLongcat, 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, modelLlama, modelLongcat, modelMinimax, modelMistral, modelNearai, modelNemotron, modelOpenaiCompatibleAlias, modelOpencode, modelQwen, modelTencent, modelXai, modelXiaomiMimo] as const satisfies readonly (readonly ModelDescriptor[])[]
export const MODEL_DESCRIPTORS = MODEL_DESCRIPTOR_GROUPS.flat() satisfies readonly ModelDescriptor[]

export { PROVIDER_PRESET_MANIFEST, ORDERED_PROVIDER_PRESETS } from './integrationManifest.generated.js'
Expand Down
16 changes: 16 additions & 0 deletions src/integrations/generated/integrationManifest.generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,21 @@ export const PROVIDER_PRESET_MANIFEST = [
"OPENAI_MODEL"
]
},
{
"preset": "longcat",
"routeKind": "vendor",
"routeId": "longcat",
"vendorId": "longcat",
"description": "LongCat OpenAI-compatible API (Meituan)",
"label": "LongCat",
"name": "LongCat",
"apiKeyEnvVars": [
"LONGCAT_API_KEY"
],
"modelEnvVars": [
"OPENAI_MODEL"
]
},
{
"preset": "minimax",
"routeKind": "vendor",
Expand Down Expand Up @@ -525,6 +540,7 @@ export const ORDERED_PROVIDER_PRESETS = [
"lmstudio",
"atomic-chat",
"ollama",
"longcat",
"minimax",
"mistral",
"moonshotai",
Expand Down
1 change: 1 addition & 0 deletions src/integrations/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ export {
getRouteProviderTypeLabel,
getTransportKindForRoute,
isCloudflareBaseUrl,
isLongcatBaseUrl,
normalizeXiaomiMimoBaseUrl,
resolveActiveRouteIdFromEnv,
resolveRouteIdFromBaseUrl,
Expand Down
21 changes: 21 additions & 0 deletions src/integrations/models/longcat.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { defineModel } from '../define.js'

export default [
defineModel({
id: 'LongCat-2.0',
label: 'LongCat-2.0',
brandId: 'longcat',
vendorId: 'longcat',
classification: ['chat', 'reasoning', 'coding'],
defaultModel: 'LongCat-2.0',
capabilities: {
supportsStreaming: true,
supportsFunctionCalling: false,
supportsJsonMode: false,
supportsReasoning: true,
supportsPreciseTokenCount: false,
},
contextWindow: 1_048_576,
maxOutputTokens: 131_072,
}),
]
23 changes: 23 additions & 0 deletions src/integrations/routeMetadata.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
getRouteDefaultModel,
getRouteProviderTypeLabel,
isCloudflareBaseUrl,
isLongcatBaseUrl,
resolveActiveRouteIdFromEnv,
resolveRouteCredentialValue,
resolveRouteIdFromBaseUrl,
Expand Down Expand Up @@ -67,6 +68,28 @@ test('isCloudflareBaseUrl matches Workers AI host but not the shared AI Gateway'
).toBe(false)
})

test('isLongcatBaseUrl requires the documented HTTPS OpenAI API path', () => {
expect(isLongcatBaseUrl('https://api.longcat.chat/openai')).toBe(true)
expect(isLongcatBaseUrl('https://api.longcat.chat/openai/')).toBe(true)
expect(isLongcatBaseUrl('https://api.longcat.chat/openai/v1')).toBe(true)
expect(isLongcatBaseUrl('https://api.longcat.chat/openai/v1/chat/completions')).toBe(true)
expect(isLongcatBaseUrl('https://api.longcat.chat/openai/chat/completions')).toBe(true)
expect(isLongcatBaseUrl('https://api.longcat.chat/openai/other')).toBe(false)
expect(isLongcatBaseUrl('https://api.longcat.chat/openai/v1?query=value')).toBe(false)
expect(isLongcatBaseUrl('https://api.longcat.chat/openai/v1#fragment')).toBe(false)
expect(isLongcatBaseUrl('https://api.longcat.chat:8443/openai/v1')).toBe(false)
expect(isLongcatBaseUrl('http://api.longcat.chat/openai/v1')).toBe(false)
expect(isLongcatBaseUrl('https://api.longcat.chat/v1')).toBe(false)
expect(isLongcatBaseUrl('https://api.longcat.chat.evil.test/openai/v1')).toBe(false)
})

test('resolveActiveRouteIdFromEnv keeps generic OpenAI credentials ahead of env-only LongCat', () => {
expect(resolveActiveRouteIdFromEnv({
OPENAI_API_KEY: 'generic-key',
LONGCAT_API_KEY: 'longcat-key',
})).not.toBe('longcat')
})

test('getRouteProviderTypeLabel uses descriptor transport kinds for provider labels', () => {
expect(getRouteProviderTypeLabel('anthropic')).toBe('Anthropic native API')
expect(getRouteProviderTypeLabel('gemini')).toBe('Gemini API')
Expand Down
71 changes: 70 additions & 1 deletion src/integrations/routeMetadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,27 @@ export function isFireworksBaseUrl(value: string | undefined): boolean {
}
}

export function isLongcatBaseUrl(value: string | undefined): boolean {
const trimmed = value?.trim()
if (!trimmed) {
return false
}

try {
const url = new URL(trimmed)
return (
url.protocol === 'https:' &&
url.hostname.toLowerCase() === 'api.longcat.chat' &&
!url.port &&
!url.search &&
!url.hash &&
/^\/openai(?:\/v1)?(?:\/chat\/completions)?\/?$/.test(url.pathname)
)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
} catch {
return false
}
}

export function isClinePassBaseUrl(value: string | undefined): boolean {
const trimmed = value?.trim()
if (!trimmed) {
Expand Down Expand Up @@ -486,6 +507,23 @@ export function getFireworksBaseUrlOverride(

return undefined
}

export function getLongcatBaseUrlOverride(
processEnv: NodeJS.ProcessEnv = process.env,
): string | undefined {
const openAIBaseUrl = processEnv.OPENAI_BASE_URL?.trim()
if (isLongcatBaseUrl(openAIBaseUrl)) {
return openAIBaseUrl
}

const openAIApiBase = processEnv.OPENAI_API_BASE?.trim()
if (isLongcatBaseUrl(openAIApiBase)) {
return openAIApiBase
}

return undefined
}

export function getMiniMaxBaseUrlOverride(
processEnv: NodeJS.ProcessEnv = process.env,
): string | undefined {
Expand Down Expand Up @@ -664,6 +702,7 @@ export function hasNearaiEnvOnlyProviderIntent(
!hasNonEmptyEnvValue(processEnv.VENICE_API_KEY) &&
!hasNonEmptyEnvValue(processEnv.MIMO_API_KEY) &&
!hasNonEmptyEnvValue(processEnv.FIREWORKS_API_KEY) &&
!hasNonEmptyEnvValue(processEnv.LONGCAT_API_KEY) &&
!hasNonEmptyEnvValue(processEnv.CLINE_API_KEY) &&
!hasConflictingOpenAIBaseUrlForRoute(processEnv, isNearaiBaseUrl) &&
hasNoExplicitNonOpenAICompatibleProvider(processEnv)
Expand All @@ -685,12 +724,31 @@ export function hasFireworksEnvOnlyProviderIntent(
!hasNonEmptyEnvValue(processEnv.VENICE_API_KEY) &&
!hasNonEmptyEnvValue(processEnv.MIMO_API_KEY) &&
!hasNonEmptyEnvValue(processEnv.NEARAI_API_KEY) &&
!hasNonEmptyEnvValue(processEnv.LONGCAT_API_KEY) &&
!hasNonEmptyEnvValue(processEnv.CLINE_API_KEY) &&
!hasConflictingOpenAIBaseUrlForRoute(processEnv, isFireworksBaseUrl) &&
hasNoExplicitNonOpenAICompatibleProvider(processEnv)
)
}

export function hasLongcatEnvOnlyProviderIntent(
processEnv: NodeJS.ProcessEnv = process.env,
): boolean {
return (
hasNonEmptyEnvValue(processEnv.LONGCAT_API_KEY) &&
!hasAnyUsableOpenAICredential(processEnv) &&
!hasNonEmptyEnvValue(processEnv.XAI_API_KEY) &&
!hasNonEmptyEnvValue(processEnv.MINIMAX_API_KEY) &&
!hasNonEmptyEnvValue(processEnv.VENICE_API_KEY) &&
!hasNonEmptyEnvValue(processEnv.MIMO_API_KEY) &&
!hasNonEmptyEnvValue(processEnv.NEARAI_API_KEY) &&
!hasNonEmptyEnvValue(processEnv.FIREWORKS_API_KEY) &&
!hasNonEmptyEnvValue(processEnv.CLINE_API_KEY) &&
!hasConflictingOpenAIBaseUrlForRoute(processEnv, isLongcatBaseUrl) &&
hasNoExplicitNonOpenAICompatibleProvider(processEnv)
)
}

/**
* Detects whether the process environment is configured to route traffic
* exclusively through ClinePass based on the presence of CLINE_API_KEY
Expand All @@ -716,6 +774,7 @@ export function resolveEnvOnlyProviderRouteId(
| 'xiaomi-mimo'
| 'nearai'
| 'fireworks'
| 'longcat'
| 'clinepass'
| null {
if (
Expand Down Expand Up @@ -753,6 +812,10 @@ export function resolveEnvOnlyProviderRouteId(
return 'fireworks'
}

if (hasLongcatEnvOnlyProviderIntent(processEnv)) {
return 'longcat'
}

if (hasClinePassEnvOnlyProviderIntent(processEnv)) {
return 'clinepass'
}
Expand Down Expand Up @@ -939,7 +1002,10 @@ export function resolveRouteIdFromBaseUrl(
// bare hostname match isn't enough for the Workers AI route — require
// the Workers AI path (/client/v4/accounts/<id>/ai/v1). Otherwise an
// unrelated Cloudflare API URL would inherit Workers-AI routing.
if (route.id === 'cloudflare' && !isCloudflareBaseUrl(baseUrl)) {
if (
(route.id === 'cloudflare' && !isCloudflareBaseUrl(baseUrl)) ||
(route.id === 'longcat' && !isLongcatBaseUrl(baseUrl))
) {
continue
}
return route.id
Expand Down Expand Up @@ -972,6 +1038,9 @@ function profileRouteHonorsBaseUrlBoundary(
if (routeId === 'cloudflare') {
return isCloudflareBaseUrl(baseUrl)
}
if (routeId === 'longcat') {
return isLongcatBaseUrl(baseUrl)
}
return true
}

Expand Down
Loading