feat(ClinePass): add gateway provider with usage support (#1818)

* feat(integrations): add ClinePass gateway provider with usage support

Adds ClinePass (https://cline.bot) as an OpenAI-compatible gateway provider.

Gateway
- New descriptor at src/integrations/gateways/clinepass.ts with 10 static models.
- Uses wireFormat: 'reasoning_effort' and full granular levels (low/medium/high/xhigh)
  so each model can expose reasoning controls consistent with Atlas Cloud.
- Dedicated credentials only: requires CLINE_API_KEY and ignores stale OPENAI_API_KEY.
- Generated integration artifacts updated via bun run integrations:generate.

/usage support
- New service module under src/services/api/clinepassUsage/ for types, fetching,
  and normalizing the ClinePass usage-limits response.
- New UI component src/components/Settings/ClinePassUsage.tsx rendered by Usage.tsx.
- Displays 5-hour, weekly, and monthly usage progress bars with longer progress bars.

Provider profile fixes
- routeMetadata.ts now resolves the active provider from the saved profile even when
  CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED is not yet set, so /usage works immediately
  after switching providers with /provider.
- providerProfile.ts / providerProfiles.ts learn CLINE_API_KEY so saved profiles apply
  the ClinePass credential alongside the OpenAI-compatible env vars.

Tests & docs
- Updated ProviderManager.test.tsx and compatibility.test.ts for the new preset.
- Added routeMetadata.test.ts cases for ClinePass and generic profile fallback.
- Added clinepassUsage.test.ts for payload normalization and row building.
- Updated .env.example and README.md with CLINE_API_KEY instructions.

Validation
- bun run typecheck
- bun run build
- bun run test:provider (964 pass)
- bun run check (5331 pass; 1 unrelated Windows file-mode failure in branch.test.ts)

* fixup: wire CLINE_API_KEY/CLINE_API_MODEL into runtime routing and profile persistence

- Add 'clinepass' to the ProviderProfile union so saved profiles can use it.
- Add CLINE_API_KEY to PROFILE_ENV_KEYS so profile switching clears stale keys.
- routeMetadata.ts: env-only CLINE_API_KEY now selects the clinepass route; other
  env-only intents treat CLINE_API_KEY as a competing credential. Added
  isClinePassBaseUrl/getClinePassBaseUrlOverride and activeProfileBaseUrl option
  to resolveActiveRouteIdFromEnv so custom/unknown profiles targeting api.cline.bot
  resolve correctly.
- providerConfig.ts: resolveProviderRequest now reads CLINE_API_MODEL when
  CLINE_API_KEY is present and defaults the base URL to https://api.cline.bot/api/v1.
- providerProfiles.ts: CLINE_API_KEY is mirrored into startup/profile env for
  clinepass and custom profiles at api.cline.bot, and is checked in
  isProcessEnvAlignedWithProfile.
- parse.ts: toIsoDate drops invalid resetsAt values instead of echoing them.
- Add fetchClinePassUsage tests covering auth, headers, non-OK responses, and errors.
- Add routeMetadata and providerConfig regression tests for ClinePass env/model wiring.
- Add providerProfiles tests for CLINE_API_KEY propagation in apply/env and startup persistence.
- Remove extra blank line in .env.example.

Validation:
- bun run typecheck
- bun run build
- bun run test:provider (972 pass)
- bun run check (5346 pass; 1 unrelated Windows file-mode failure in branch.test.ts)

* fixup: address PR review findings for ClinePass env routing and profile persistence

- routeMetadata.ts: Let the concrete OPENAI_BASE_URL match win before falling
  back to activeProfileProvider / activeProfileBaseUrl. Prevents a saved ClinePass
  profile from overriding an explicit CLAUDE_CODE_USE_OPENAI env pointing at
  another gateway.
- providerConfig.ts: Gate the ClinePass model branch behind !isGithubMode so a
  stale CLINE_API_KEY/CLINE_API_MODEL does not override GitHub Copilot model
  selection.
- providerProfiles.ts: Introduce isClinePassProfile() predicate that uses route
  resolution (routeId === 'clinepass' || baseUrl includes api.cline.bot) and
  share it across live application, alignment, and startup persistence paths so
  saved ClinePass profiles keep the dedicated credential consistently even with
  non-default base URLs.
- Add regression tests covering all three findings.

Validation:
- bun run typecheck
- bun run build
- bun run test:provider (973 pass)

* fixup: use hostname-based ClinePass detection in providerProfiles

Replace includes('api.cline.bot') substring matching with isClinePassBaseUrl
which validates the exact hostname via URL parsing, preventing spoofed hosts
like api.cline.bot.evil.example from triggering CLINE_API_KEY mirroring.

Validation:
- bun run typecheck
- bun run build
- bun run test:provider (973 pass)

* fixup: gate ClinePass model selection on resolved base URL

Move base URL resolution before model selection in resolveProviderRequest
so effectiveClinePassMode is only active when no explicit non-ClinePass base
URL is set via options.baseUrl, OPENAI_BASE_URL, or OPENAI_API_BASE.

Previously CLINE_API_KEY=cp-key + CLINE_API_MODEL=cline-pass/qwen3.7-max
+ OPENAI_BASE_URL=https://api.openai.com/v1 would return
requestedModel=cline-pass/qwen3.7-max with baseUrl=https://api.openai.com/v1,
sending a ClinePass model ID to a non-Cline provider.

Now the resolver returns the correct OPENAI_MODEL and OpenAI base URL in that
scenario, and only uses ClinePass model/default-base when the resolved base
URL is absent or actually api.cline.bot.

Added regression tests for:
- CLINE_API_KEY + CLINE_API_MODEL + explicit OPENAI_BASE_URL
- CLINE_API_KEY + CLINE_API_MODEL + explicit baseUrl option
- CLINE_API_KEY + CLINE_API_MODEL with no base URL (ClinePass default)

Validation:
- bun run typecheck
- bun run build
- bun run test:provider (976 pass)

* fixup: default ClinePass model for blank env

Treat whitespace-only CLINE_API_MODEL as unset so OPENAI_MODEL can still provide the ClinePass model override.

When CLINE_API_KEY selects ClinePass without any model env, fall back to the ClinePass route default instead of the generic codexplan alias.

Validation:

- timeout 600 bun test src/services/api/providerConfig.test.ts

- timeout 600 bun test src/services/api/clinepassUsage.test.ts src/integrations/routeMetadata.test.ts src/services/api/providerConfig.test.ts src/utils/providerProfiles.test.ts src/integrations/compatibility.test.ts src/components/ProviderManager.test.tsx

- timeout 600 bun test src/services/api/providerConfig.test.ts src/services/api/client.test.ts src/integrations/routeMetadata.test.ts src/integrations/runtimeMetadata.test.ts src/services/api/clinepassUsage.test.ts src/services/api/minimaxUsage.test.ts src/utils/providerProfiles.test.ts

- timeout 600 bun run integrations:check

- timeout 600 bun run typecheck

- timeout 600 bun run build

- timeout 600 bun run security:pr-scan

- git diff --check origin/main...HEAD
This commit is contained in:
JATMN
2026-06-30 08:29:02 +08:00
committed by GitHub
parent d0843bed0b
commit 985984b9ff
23 changed files with 1501 additions and 52 deletions
+12
View File
@@ -352,6 +352,18 @@ ANTHROPIC_API_KEY=sk-ant-your-key-here
# OPENAI_MODEL=accounts/fireworks/models/llama-v3p1-70b-instruct
# -----------------------------------------------------------------------------
# Option 13: 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
# Models include DeepSeek, Qwen, GLM, Kimi, MiMo, and MiniMax.
#
# CLINE_API_KEY=your-cline-api-key-here
# CLINE_API_MODEL=cline-pass/deepseek-v4-flash (optional, default)
#
# Usage limits are displayed via /usage command.
# =============================================================================
# OPTIONAL TUNING
# =============================================================================
+1
View File
@@ -226,6 +226,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` |
| 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` |
| 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 |
| Codex OAuth | `/provider` | Opens ChatGPT sign-in in your browser and stores Codex credentials securely |
+2
View File
@@ -47,6 +47,7 @@ export function resolveActiveUsageId(
processEnv: NodeJS.ProcessEnv = process.env,
options?: {
activeProfileProvider?: string
activeProfileBaseUrl?: string
providerCategory?: APIProvider | string
},
): APIProvider | string {
@@ -62,6 +63,7 @@ export function resolveActiveUsageId(
const routeId = resolveActiveRouteIdFromEnv(processEnv, {
activeProfileProvider: options?.activeProfileProvider,
activeProfileBaseUrl: options?.activeProfileBaseUrl,
})
return routeId ?? providerCategory ?? 'firstParty'
+1
View File
@@ -116,6 +116,7 @@ const PRESET_ORDER = [
'Atlas Cloud',
'Azure OpenAI',
'Bankr',
'ClinePass',
'DeepSeek',
'Codex OAuth',
'xAI OAuth (Grok)',
+219
View File
@@ -0,0 +1,219 @@
import * as React from 'react'
import { useEffect, useState } from 'react'
import { useTerminalSize } from '../../hooks/useTerminalSize.js'
import { Box, Text } from '../../ink.js'
import { useKeybinding } from '../../keybindings/useKeybinding.js'
import {
buildClinePassUsageRows,
fetchClinePassUsage,
type ClinePassUsageData,
type ClinePassUsageRow,
} from '../../services/api/clinepassUsage.js'
import { logError } from '../../utils/log.js'
import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.js'
import { Byline } from '../design-system/Byline.js'
import { ProgressBar } from '../design-system/ProgressBar.js'
const RESET_COUNTDOWN_REFRESH_MS = 30_000
const PROGRESS_BAR_WIDTH = 40
type ClinePassUsageLimitBarProps = {
label: string
usedPercent: number
resetsAt?: string
maxWidth: number
nowMs: number
}
function formatCountdownDuration(ms: number): string {
const totalMinutes = Math.max(1, Math.ceil(ms / 60_000))
const days = Math.floor(totalMinutes / 1_440)
const hours = Math.floor((totalMinutes % 1_440) / 60)
const minutes = totalMinutes % 60
if (days > 0) {
return hours > 0 ? `${days}d ${hours}h` : `${days}d`
}
if (hours > 0) {
return minutes > 0 ? `${hours}h ${minutes}m` : `${hours}h`
}
return `${minutes}m`
}
function formatResetCountdown(
resetsAt: string | undefined,
nowMs: number,
): string | undefined {
if (!resetsAt) return undefined
const resetMs = Date.parse(resetsAt)
if (!Number.isFinite(resetMs)) return undefined
const remainingMs = resetMs - nowMs
if (remainingMs <= 0) {
return 'Resetting now'
}
return `Resets in ${formatCountdownDuration(remainingMs)}`
}
function ClinePassUsageLimitBar({
label,
usedPercent,
resetsAt,
maxWidth,
nowMs,
}: ClinePassUsageLimitBarProps): React.ReactNode {
const normalizedUsedPercent = Math.max(0, Math.min(100, usedPercent))
const usedText = `${Math.floor(normalizedUsedPercent)}% used`
const resetText = formatResetCountdown(resetsAt, nowMs)
return (
<Box flexDirection="column">
<Text>
<Text bold>{label}</Text>
{resetText ? <Text dimColor> · {resetText}</Text> : null}
</Text>
<Box flexDirection="row" gap={1}>
<ProgressBar
ratio={normalizedUsedPercent / 100}
width={Math.min(PROGRESS_BAR_WIDTH, Math.max(1, maxWidth))}
fillColor="rate_limit_fill"
emptyColor="rate_limit_empty"
/>
<Text dimColor>{usedText}</Text>
</Box>
</Box>
)
}
export function ClinePassUsage(): React.ReactNode {
const [usage, setUsage] = useState<ClinePassUsageData | null>(null)
const [error, setError] = useState<string | null>(null)
const [isLoading, setIsLoading] = useState(true)
const [nowMs, setNowMs] = useState(() => Date.now())
const { columns } = useTerminalSize()
const availableWidth = columns - 2
const maxWidth = Math.min(availableWidth, 80)
const loadUsage = React.useCallback(async () => {
setIsLoading(true)
setError(null)
try {
setUsage(await fetchClinePassUsage())
} catch (err) {
logError(err as Error)
setError(
err instanceof Error ? err.message : 'Failed to load ClinePass usage',
)
} finally {
setIsLoading(false)
}
}, [])
useEffect(() => {
void loadUsage()
}, [loadUsage])
useEffect(() => {
const interval = setInterval(() => {
setNowMs(Date.now())
}, RESET_COUNTDOWN_REFRESH_MS)
return () => clearInterval(interval)
}, [])
useKeybinding(
'settings:retry',
() => {
void loadUsage()
},
{
context: 'Settings',
isActive: !!error && !isLoading,
},
)
if (error) {
return (
<Box flexDirection="column" gap={1}>
<Text color="error">Error: {error}</Text>
<Text dimColor>
<Byline>
<ConfigurableShortcutHint
action="settings:retry"
context="Settings"
fallback="r"
description="retry"
/>
<ConfigurableShortcutHint
action="confirm:no"
context="Settings"
fallback="Esc"
description="cancel"
/>
</Byline>
</Text>
</Box>
)
}
if (!usage) {
return (
<Box flexDirection="column" gap={1}>
<Text dimColor>Loading ClinePass usage data</Text>
<Text dimColor>
<ConfigurableShortcutHint
action="confirm:no"
context="Settings"
fallback="Esc"
description="cancel"
/>
</Text>
</Box>
)
}
const rows =
usage.availability === 'available'
? buildClinePassUsageRows(usage.windows)
: []
return (
<Box flexDirection="column" gap={1} width="100%">
{usage.planName ? <Text dimColor>Plan: {usage.planName}</Text> : null}
{usage.availability === 'unknown' ? (
<Text dimColor>{usage.message}</Text>
) : rows.length === 0 ? (
<Text dimColor>
No ClinePass usage windows were returned for this account.
</Text>
) : null}
{rows.map((row, index) => (
<ClinePassUsageLimitBar
key={`${row.label}-${index}`}
label={row.label}
usedPercent={row.usedPercent}
resetsAt={row.resetsAt}
maxWidth={maxWidth}
nowMs={nowMs}
/>
))}
<Text dimColor>
<ConfigurableShortcutHint
action="confirm:no"
context="Settings"
fallback="Esc"
description="cancel"
/>
</Text>
</Box>
)
}
+5
View File
@@ -22,6 +22,7 @@ import { Byline } from '../design-system/Byline.js';
import { ProgressBar } from '../design-system/ProgressBar.js';
import { isEligibleForOverageCreditGrant, OverageCreditUpsell } from '../LogoV2/OverageCreditUpsell.js';
import { CodexUsage } from './CodexUsage.js';
import { ClinePassUsage } from './ClinePassUsage.js';
import { MiniMaxUsage } from './MiniMaxUsage.js';
import { UnsupportedUsage } from './UnsupportedUsage.js';
type LimitBarProps = {
@@ -277,6 +278,7 @@ export function Usage(): React.ReactNode {
const activeProfile = getActiveProviderProfile();
const usageDescriptor = getUsageDescriptor(resolveActiveUsageId(process.env, {
activeProfileProvider: activeProfile?.provider,
activeProfileBaseUrl: activeProfile?.baseUrl,
providerCategory: provider,
}));
if (provider === 'codex') {
@@ -285,6 +287,9 @@ export function Usage(): React.ReactNode {
if (usageDescriptor.resolvedId === 'minimax' && usageDescriptor.supported) {
return <MiniMaxUsage />;
}
if (usageDescriptor.resolvedId === 'clinepass' && usageDescriptor.supported) {
return <ClinePassUsage />;
}
if (usageDescriptor.resolvedId === 'anthropic' && usageDescriptor.supported) {
return <AnthropicUsage />;
}
+1
View File
@@ -48,6 +48,7 @@ const EXPECTED_PRESETS = [
'fireworks',
'opencode',
'opencode-go',
'clinepass',
] as const satisfies readonly ProviderPreset[]
describe('compatibility mappings', () => {
+230
View File
@@ -0,0 +1,230 @@
import { defineGateway } from '../define.js'
export default defineGateway({
id: 'clinepass',
label: 'ClinePass',
vendorId: 'openai',
category: 'hosted',
defaultBaseUrl: 'https://api.cline.bot/api/v1',
defaultModel: 'cline-pass/deepseek-v4-flash',
setup: {
requiresAuth: true,
authMode: 'api-key',
credentialEnvVars: ['CLINE_API_KEY'],
dedicatedCredentialsOnly: true,
},
transportConfig: {
kind: 'openai-compatible',
},
preset: {
id: 'clinepass',
vendorId: 'openai',
description: 'ClinePass - AI model gateway with usage limits',
apiKeyEnvVars: ['CLINE_API_KEY'],
modelEnvVars: ['CLINE_API_MODEL', 'OPENAI_MODEL'],
},
validation: {
kind: 'credential-env',
routing: {
matchDefaultBaseUrl: true,
},
credentialEnvVars: ['CLINE_API_KEY'],
missingCredentialMessage:
'ClinePass auth is required. Set CLINE_API_KEY.',
},
catalog: {
source: 'static',
models: [
{
id: 'cline-pass/qwen3.7-max',
apiName: 'cline-pass/qwen3.7-max',
aliases: ['qwen3.7-max'],
modelDescriptorId: 'qwen3.7-max',
label: 'Qwen3.7 Max',
contextWindow: 1_000_000,
maxOutputTokens: 67_072,
capabilities: {
supportsFunctionCalling: true,
supportsReasoning: true,
},
reasoning: {
mode: 'levels',
levels: ['low', 'medium', 'high', 'xhigh'],
wireFormat: 'reasoning_effort',
},
},
{
id: 'cline-pass/qwen3.7-plus',
apiName: 'cline-pass/qwen3.7-plus',
aliases: ['qwen3.7-plus'],
modelDescriptorId: 'qwen3.7-plus',
label: 'Qwen3.7 Plus',
contextWindow: 1_000_000,
maxOutputTokens: 67_072,
capabilities: {
supportsVision: true,
supportsFunctionCalling: true,
supportsReasoning: true,
},
reasoning: {
mode: 'levels',
levels: ['low', 'medium', 'high', 'xhigh'],
wireFormat: 'reasoning_effort',
},
},
{
id: 'cline-pass/minimax-m3',
apiName: 'cline-pass/minimax-m3',
aliases: ['minimax-m3'],
modelDescriptorId: 'minimax-m3',
label: 'MiniMax M3',
contextWindow: 524_288,
maxOutputTokens: 524_288,
capabilities: {
supportsVision: true,
supportsFunctionCalling: true,
supportsReasoning: false,
},
},
{
id: 'cline-pass/glm-5.2',
apiName: 'cline-pass/glm-5.2',
aliases: ['glm-5.2'],
modelDescriptorId: 'glm-5.2',
label: 'GLM 5.2',
contextWindow: 202_752,
maxOutputTokens: 202_752,
capabilities: {
supportsVision: true,
supportsFunctionCalling: true,
supportsReasoning: true,
},
reasoning: {
mode: 'levels',
levels: ['low', 'medium', 'high', 'xhigh'],
wireFormat: 'reasoning_effort',
},
},
{
id: 'cline-pass/kimi-k2.7-code',
apiName: 'cline-pass/kimi-k2.7-code',
aliases: ['kimi-k2.7-code'],
modelDescriptorId: 'kimi-k2.7-code',
label: 'Kimi K2.7 Code',
contextWindow: 262_144,
maxOutputTokens: 32_768,
capabilities: {
supportsVision: true,
supportsFunctionCalling: true,
supportsReasoning: true,
},
reasoning: {
mode: 'levels',
levels: ['low', 'medium', 'high', 'xhigh'],
wireFormat: 'reasoning_effort',
},
},
{
id: 'cline-pass/kimi-k2.6',
apiName: 'cline-pass/kimi-k2.6',
aliases: ['kimi-k2.6'],
modelDescriptorId: 'kimi-k2.6',
label: 'Kimi K2.6',
contextWindow: 262_144,
maxOutputTokens: 262_144,
capabilities: {
supportsVision: true,
supportsFunctionCalling: true,
supportsReasoning: true,
},
reasoning: {
mode: 'levels',
levels: ['low', 'medium', 'high', 'xhigh'],
wireFormat: 'reasoning_effort',
},
},
{
id: 'cline-pass/deepseek-v4-pro',
apiName: 'cline-pass/deepseek-v4-pro',
aliases: ['deepseek-v4-pro'],
modelDescriptorId: 'deepseek-v4-pro',
label: 'DeepSeek V4 Pro',
contextWindow: 1_048_576,
maxOutputTokens: 393_216,
capabilities: {
supportsVision: true,
supportsFunctionCalling: true,
supportsReasoning: true,
},
reasoning: {
mode: 'levels',
levels: ['low', 'medium', 'high', 'xhigh'],
wireFormat: 'reasoning_effort',
},
},
{
id: 'cline-pass/deepseek-v4-flash',
apiName: 'cline-pass/deepseek-v4-flash',
aliases: ['deepseek-v4-flash'],
modelDescriptorId: 'deepseek-v4-flash',
label: 'DeepSeek V4 Flash',
contextWindow: 1_048_576,
maxOutputTokens: 393_216,
capabilities: {
supportsVision: true,
supportsFunctionCalling: true,
supportsReasoning: true,
},
reasoning: {
mode: 'levels',
levels: ['low', 'medium', 'high', 'xhigh'],
wireFormat: 'reasoning_effort',
},
},
{
id: 'cline-pass/mimo-v2.5',
apiName: 'cline-pass/mimo-v2.5',
aliases: ['mimo-v2.5'],
modelDescriptorId: 'mimo-v2.5',
label: 'MiMo V2.5',
contextWindow: 1_048_576,
maxOutputTokens: 65_536,
capabilities: {
supportsVision: true,
supportsFunctionCalling: true,
supportsReasoning: true,
},
reasoning: {
mode: 'levels',
levels: ['low', 'medium', 'high', 'xhigh'],
wireFormat: 'reasoning_effort',
},
},
{
id: 'cline-pass/mimo-v2.5-pro',
apiName: 'cline-pass/mimo-v2.5-pro',
aliases: ['mimo-v2.5-pro'],
modelDescriptorId: 'mimo-v2.5-pro',
label: 'MiMo V2.5 Pro',
contextWindow: 1_048_576,
maxOutputTokens: 65_536,
capabilities: {
supportsFunctionCalling: true,
supportsReasoning: true,
},
reasoning: {
mode: 'levels',
levels: ['low', 'medium', 'high', 'xhigh'],
wireFormat: 'reasoning_effort',
},
},
],
},
usage: {
supported: true,
ui: {
showResetCountdown: true,
compactProgressBar: false,
},
},
})
@@ -22,6 +22,7 @@ import gatewayAtlasCloud from '../gateways/atlas-cloud.js'
import gatewayAtomicChat from '../gateways/atomic-chat.js'
import gatewayAzureOpenai from '../gateways/azure-openai.js'
import gatewayBedrock from '../gateways/bedrock.js'
import gatewayClinepass from '../gateways/clinepass.js'
import gatewayCustom from '../gateways/custom.js'
import gatewayDashscopeCn from '../gateways/dashscope-cn.js'
import gatewayDashscopeIntl from '../gateways/dashscope-intl.js'
@@ -76,7 +77,7 @@ 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 GATEWAY_DESCRIPTORS = [gatewayAtlasCloud, gatewayAtomicChat, gatewayAzureOpenai, gatewayBedrock, 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 = [gatewayAtlasCloud, gatewayAtomicChat, gatewayAzureOpenai, gatewayBedrock, gatewayClinepass, 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 = [] as const satisfies readonly AnthropicProxyDescriptor[]
export const BRAND_DESCRIPTORS = [brandClaude, brandDeepseek, brandFireworks, brandGemini, brandGlm, brandGpt, brandKimi, brandLlama, brandMinimax, brandMistral, brandNearai, brandNemotron, brandOpenaiCompatibleAlias, brandQwen, brandXai, brandXiaomiMimo] as const satisfies readonly BrandDescriptor[]
export const MODEL_DESCRIPTOR_GROUPS = [modelClaude, modelDeepseek, modelFireworksMerged, modelGemini, modelGlm, modelGpt, modelKimi, modelLlama, modelMinimax, modelMistral, modelNearai, modelNemotron, modelOpenaiCompatibleAlias, modelOpencode, modelQwen, modelXai, modelXiaomiMimo] as const satisfies readonly (readonly ModelDescriptor[])[]
@@ -112,6 +112,21 @@ export const PROVIDER_PRESET_MANIFEST = [
"OPENAI_MODEL"
]
},
{
"preset": "clinepass",
"routeKind": "gateway",
"routeId": "clinepass",
"vendorId": "openai",
"gatewayId": "clinepass",
"description": "ClinePass - AI model gateway with usage limits",
"apiKeyEnvVars": [
"CLINE_API_KEY"
],
"modelEnvVars": [
"CLINE_API_MODEL",
"OPENAI_MODEL"
]
},
{
"preset": "deepseek",
"routeKind": "vendor",
@@ -444,6 +459,7 @@ export const ORDERED_PROVIDER_PRESETS = [
"atlas-cloud",
"azure-openai",
"bankr",
"clinepass",
"deepseek",
"fireworks",
"gemini",
+113
View File
@@ -206,6 +206,23 @@ test('resolveActiveRouteIdFromEnv treats xAI credential-only env as xAI', () =>
).toBe('xai')
})
test('resolveActiveRouteIdFromEnv treats ClinePass credential-only env as ClinePass', () => {
expect(
resolveActiveRouteIdFromEnv({
CLINE_API_KEY: 'cline-key',
}),
).toBe('clinepass')
})
test('resolveActiveRouteIdFromEnv prefers ClinePass key over Fireworks env-only intent', () => {
expect(
resolveActiveRouteIdFromEnv({
CLINE_API_KEY: 'cline-key',
FIREWORKS_API_KEY: 'fw-key',
}),
).toBe('clinepass')
})
test('resolveActiveRouteIdFromEnv prefers xAI when env-only keys compete', () => {
expect(
resolveActiveRouteIdFromEnv({
@@ -285,6 +302,102 @@ test.each([
},
)
test('resolveActiveRouteIdFromEnv refines generic OpenAI profile by ClinePass base URL', () => {
expect(
resolveActiveRouteIdFromEnv(
{
CLAUDE_CODE_USE_OPENAI: '1',
OPENAI_BASE_URL: 'https://api.cline.bot/api/v1',
OPENAI_MODEL: 'cline-pass/deepseek-v4-flash',
},
{ activeProfileProvider: 'openai' },
),
).toBe('clinepass')
})
test('resolveActiveRouteIdFromEnv resolves ClinePass profile provider without env applied flag', () => {
expect(
resolveActiveRouteIdFromEnv(
{},
{ activeProfileProvider: 'clinepass' },
),
).toBe('clinepass')
})
test('resolveActiveRouteIdFromEnv resolves ClinePass profile provider without CLAUDE_CODE_USE_OPENAI', () => {
expect(
resolveActiveRouteIdFromEnv(
{
CLINE_API_KEY: 'cp-key',
},
{ activeProfileProvider: 'clinepass' },
),
).toBe('clinepass')
})
test('resolveActiveRouteIdFromEnv still returns anthropic when no env flags and no profile provider', () => {
expect(resolveActiveRouteIdFromEnv({})).toBe('anthropic')
})
test('resolveActiveRouteIdFromEnv resolves Atlas Cloud profile provider without env applied flag', () => {
expect(
resolveActiveRouteIdFromEnv(
{},
{ activeProfileProvider: 'atlas-cloud' },
),
).toBe('atlas-cloud')
})
test('resolveActiveRouteIdFromEnv does not resolve custom profile provider as a known route', () => {
expect(
resolveActiveRouteIdFromEnv(
{},
{ activeProfileProvider: 'custom' },
),
).toBe('anthropic')
})
test('resolveActiveRouteIdFromEnv resolves custom profile provider via ClinePass base URL', () => {
expect(
resolveActiveRouteIdFromEnv(
{},
{
activeProfileProvider: 'custom',
activeProfileBaseUrl: 'https://api.cline.bot/api/v1',
},
),
).toBe('clinepass')
})
test('resolveActiveRouteIdFromEnv resolves openai profile provider via ClinePass base URL', () => {
expect(
resolveActiveRouteIdFromEnv(
{
CLAUDE_CODE_USE_OPENAI: '1',
},
{
activeProfileProvider: 'openai',
activeProfileBaseUrl: 'https://api.cline.bot/api/v1',
},
),
).toBe('clinepass')
})
test('resolveActiveRouteIdFromEnv lets explicit OPENAI_BASE_URL override saved ClinePass profile', () => {
expect(
resolveActiveRouteIdFromEnv(
{
CLAUDE_CODE_USE_OPENAI: '1',
OPENAI_BASE_URL: 'https://openrouter.ai/api/v1',
},
{
activeProfileProvider: 'clinepass',
activeProfileBaseUrl: 'https://api.cline.bot/api/v1',
},
),
).toBe('openrouter')
})
test('resolveActiveRouteIdFromEnv does not infer MiniMax with OpenAI credentials', () => {
expect(
resolveActiveRouteIdFromEnv({
+88 -9
View File
@@ -361,6 +361,35 @@ export function isFireworksBaseUrl(value: string | undefined): boolean {
}
}
export function isClinePassBaseUrl(value: string | undefined): boolean {
const trimmed = value?.trim()
if (!trimmed) {
return false
}
try {
return new URL(trimmed).hostname.toLowerCase() === 'api.cline.bot'
} catch {
return false
}
}
export function getClinePassBaseUrlOverride(
processEnv: NodeJS.ProcessEnv = process.env,
): string | undefined {
const openAIBaseUrl = processEnv.OPENAI_BASE_URL?.trim()
if (isClinePassBaseUrl(openAIBaseUrl)) {
return openAIBaseUrl
}
const openAIApiBase = processEnv.OPENAI_API_BASE?.trim()
if (isClinePassBaseUrl(openAIApiBase)) {
return openAIApiBase
}
return undefined
}
export function getNearaiBaseUrlOverride(
processEnv: NodeJS.ProcessEnv = process.env,
): string | undefined {
@@ -482,6 +511,7 @@ export function hasXaiEnvOnlyProviderIntent(
): boolean {
return (
hasNonEmptyEnvValue(processEnv.XAI_API_KEY) &&
!hasNonEmptyEnvValue(processEnv.CLINE_API_KEY) &&
!hasConflictingOpenAIBaseUrlForRoute(processEnv, isXaiBaseUrl) &&
hasNoExplicitNonOpenAICompatibleProvider(processEnv)
)
@@ -502,6 +532,7 @@ export function hasMiniMaxEnvOnlyProviderIntent(
(hasExplicitMiniMaxIntent ||
(!hasAnyUsableOpenAICredential(processEnv) &&
!hasNonEmptyEnvValue(processEnv.XAI_API_KEY) &&
!hasNonEmptyEnvValue(processEnv.CLINE_API_KEY) &&
hasNoExplicitNonOpenAICompatibleProvider(processEnv)))
)
}
@@ -514,6 +545,7 @@ export function hasVeniceEnvOnlyProviderIntent(
!hasAnyUsableOpenAICredential(processEnv) &&
!hasNonEmptyEnvValue(processEnv.XAI_API_KEY) &&
!hasNonEmptyEnvValue(processEnv.MINIMAX_API_KEY) &&
!hasNonEmptyEnvValue(processEnv.CLINE_API_KEY) &&
!hasConflictingOpenAIBaseUrlForRoute(processEnv, isVeniceBaseUrl) &&
hasNoExplicitNonOpenAICompatibleProvider(processEnv)
)
@@ -528,6 +560,7 @@ export function hasXiaomiMimoEnvOnlyProviderIntent(
!hasNonEmptyEnvValue(processEnv.XAI_API_KEY) &&
!hasNonEmptyEnvValue(processEnv.MINIMAX_API_KEY) &&
!hasNonEmptyEnvValue(processEnv.VENICE_API_KEY) &&
!hasNonEmptyEnvValue(processEnv.CLINE_API_KEY) &&
!hasConflictingOpenAIBaseUrlForRoute(processEnv, isXiaomiMimoBaseUrl) &&
hasNoExplicitNonOpenAICompatibleProvider(processEnv)
)
@@ -543,6 +576,7 @@ export function hasNearaiEnvOnlyProviderIntent(
!hasNonEmptyEnvValue(processEnv.VENICE_API_KEY) &&
!hasNonEmptyEnvValue(processEnv.MIMO_API_KEY) &&
!hasNonEmptyEnvValue(processEnv.FIREWORKS_API_KEY) &&
!hasNonEmptyEnvValue(processEnv.CLINE_API_KEY) &&
!hasConflictingOpenAIBaseUrlForRoute(processEnv, isNearaiBaseUrl) &&
hasNoExplicitNonOpenAICompatibleProvider(processEnv)
)
@@ -563,14 +597,30 @@ export function hasFireworksEnvOnlyProviderIntent(
!hasNonEmptyEnvValue(processEnv.VENICE_API_KEY) &&
!hasNonEmptyEnvValue(processEnv.MIMO_API_KEY) &&
!hasNonEmptyEnvValue(processEnv.NEARAI_API_KEY) &&
!hasNonEmptyEnvValue(processEnv.CLINE_API_KEY) &&
!hasConflictingOpenAIBaseUrlForRoute(processEnv, isFireworksBaseUrl) &&
hasNoExplicitNonOpenAICompatibleProvider(processEnv)
)
}
/**
* Detects whether the process environment is configured to route traffic
* exclusively through ClinePass based on the presence of CLINE_API_KEY
* and the absence of conflicting env vars for other providers.
*/
export function hasClinePassEnvOnlyProviderIntent(
processEnv: NodeJS.ProcessEnv = process.env,
): boolean {
return (
hasNonEmptyEnvValue(processEnv.CLINE_API_KEY) &&
!hasConflictingOpenAIBaseUrlForRoute(processEnv, isClinePassBaseUrl) &&
hasNoExplicitNonOpenAICompatibleProvider(processEnv)
)
}
export function resolveEnvOnlyProviderRouteId(
processEnv: NodeJS.ProcessEnv = process.env,
): 'xai' | 'minimax' | 'venice' | 'xiaomi-mimo' | 'nearai' | 'fireworks' | null {
): 'xai' | 'minimax' | 'venice' | 'xiaomi-mimo' | 'nearai' | 'fireworks' | 'clinepass' | null {
if (
hasMiniMaxRouteIntent(processEnv) &&
hasMiniMaxEnvOnlyProviderIntent(processEnv)
@@ -602,6 +652,10 @@ export function resolveEnvOnlyProviderRouteId(
return 'fireworks'
}
if (hasClinePassEnvOnlyProviderIntent(processEnv)) {
return 'clinepass'
}
return null
}
@@ -794,6 +848,7 @@ export function resolveActiveRouteIdFromEnv(
processEnv: NodeJS.ProcessEnv = process.env,
options?: {
activeProfileProvider?: string
activeProfileBaseUrl?: string
},
): string | null {
if (isEnvTruthy(processEnv.CLAUDE_CODE_USE_GEMINI)) {
@@ -820,10 +875,11 @@ export function resolveActiveRouteIdFromEnv(
processEnv.OPENAI_BASE_URL ?? processEnv.OPENAI_API_BASE
const matchedRoute = resolveRouteIdFromBaseUrl(baseUrl)
if (
processEnv.CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED === '1' &&
options?.activeProfileProvider
) {
if (matchedRoute) {
return matchedRoute
}
if (options?.activeProfileProvider) {
const route = resolveProfileRoute(options.activeProfileProvider)
if (
route.routeId !== 'unknown-fallback' &&
@@ -832,10 +888,14 @@ export function resolveActiveRouteIdFromEnv(
) {
return route.routeId
}
}
if (matchedRoute) {
return matchedRoute
// 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(
options.activeProfileBaseUrl,
)
if (profileBaseUrlRoute) {
return profileBaseUrlRoute
}
}
const normalizedBaseUrl = normalizeComparableBaseUrl(baseUrl)
@@ -850,6 +910,25 @@ export function resolveActiveRouteIdFromEnv(
return 'custom'
}
if (options?.activeProfileProvider) {
const route = resolveProfileRoute(options.activeProfileProvider)
if (
route.routeId !== 'unknown-fallback' &&
route.routeId !== 'openai' &&
route.routeId !== 'custom'
) {
return route.routeId
}
// 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(
options.activeProfileBaseUrl,
)
if (profileBaseUrlRoute) {
return profileBaseUrlRoute
}
}
return 'anthropic'
}
+3 -1
View File
@@ -245,6 +245,7 @@ export function resolveOpenAIShimRuntimeContext(options?: {
const activeRouteId = resolveActiveRouteIdFromEnv(runtimeEnv, {
activeProfileProvider: options?.activeProfileProvider,
activeProfileBaseUrl: options?.baseUrl,
})
const baseUrlRouteId = resolveRouteIdFromBaseUrl(options?.baseUrl)
const routeId =
@@ -438,7 +439,8 @@ export function resolveModelRuntimeLimits(options: {
}
const routeId = resolveActiveRouteIdFromEnv(runtimeEnv, {
activeProfileProvider: options.activeProfileProvider,
activeProfileProvider: options?.activeProfileProvider,
activeProfileBaseUrl: options?.baseUrl,
})
const modelApiName = getBaseModelApiName(options.model) ?? options.model
const catalogEntry = findCatalogEntryForApiName(routeId, modelApiName)
+190
View File
@@ -0,0 +1,190 @@
import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test'
import {
buildClinePassUsageRows,
fetchClinePassUsage,
normalizeClinePassUsagePayload,
} from './clinepassUsage.js'
import { getClinePassUsageUrl } from './clinepassUsage/fetch.js'
describe('normalizeClinePassUsagePayload', () => {
test('normalizes usage limits from captured response shape', () => {
const normalized = normalizeClinePassUsagePayload({
data: {
limits: [
{ type: 'five_hour', percentUsed: 86, resetsAt: '2026-06-29T22:00:00.000Z' },
{ type: 'weekly', percentUsed: 34, resetsAt: '2026-07-07T00:00:00.000Z' },
{ type: 'monthly', percentUsed: 17, resetsAt: '2026-07-29T00:00:00.000Z' },
],
},
})
expect(normalized).toEqual({
availability: 'available',
planName: undefined,
windows: [
{ label: '5-Hour Limit', type: 'five_hour', usedPercent: 86, resetsAt: '2026-06-29T22:00:00.000Z' },
{ label: 'Weekly Limit', type: 'weekly', usedPercent: 34, resetsAt: '2026-07-07T00:00:00.000Z' },
{ label: 'Monthly Limit', type: 'monthly', usedPercent: 17, resetsAt: '2026-07-29T00:00:00.000Z' },
],
})
})
test('clamps percent values to [0, 100]', () => {
const normalized = normalizeClinePassUsagePayload({
data: {
limits: [
{ type: 'five_hour', percentUsed: -5, resetsAt: '2026-06-29T22:00:00.000Z' },
{ type: 'weekly', percentUsed: 150, resetsAt: '2026-07-07T00:00:00.000Z' },
],
},
})
expect(normalized).toEqual({
availability: 'available',
planName: undefined,
windows: [
{ label: '5-Hour Limit', type: 'five_hour', usedPercent: 0, resetsAt: '2026-06-29T22:00:00.000Z' },
{ label: 'Weekly Limit', type: 'weekly', usedPercent: 100, resetsAt: '2026-07-07T00:00:00.000Z' },
],
})
})
test('rounds fractional percent values', () => {
const normalized = normalizeClinePassUsagePayload({
data: {
limits: [{ type: 'five_hour', percentUsed: 86.7, resetsAt: '2026-06-29T22:00:00.000Z' }],
},
})
expect(normalized).toMatchObject({
availability: 'available',
windows: [{ label: '5-Hour Limit', usedPercent: 87 }],
})
})
test('returns unknown availability when no limits are present', () => {
const normalized = normalizeClinePassUsagePayload({
data: { limits: [] },
})
expect(normalized).toMatchObject({
availability: 'unknown',
windows: [],
})
})
test('returns unknown availability for non-record payload', () => {
const normalized = normalizeClinePassUsagePayload(null)
expect(normalized).toMatchObject({
availability: 'unknown',
windows: [],
})
})
test('drops invalid resetsAt values instead of echoing them', () => {
const normalized = normalizeClinePassUsagePayload({
data: {
limits: [{ type: 'five_hour', percentUsed: 10, resetsAt: 'not-a-date' }],
},
})
expect(normalized).toMatchObject({
availability: 'available',
windows: [{ label: '5-Hour Limit', usedPercent: 10, resetsAt: undefined }],
})
})
})
describe('buildClinePassUsageRows', () => {
test('builds window rows from normalized windows', () => {
const rows = buildClinePassUsageRows([
{ label: '5-Hour Limit', type: 'five_hour', usedPercent: 86, resetsAt: '2026-06-29T22:00:00.000Z' },
{ label: 'Weekly Limit', type: 'weekly', usedPercent: 34 },
])
expect(rows).toEqual([
{ kind: 'window', label: '5-Hour Limit', usedPercent: 86, resetsAt: '2026-06-29T22:00:00.000Z' },
{ kind: 'window', label: 'Weekly Limit', usedPercent: 34 },
])
})
})
describe('ClinePass usage helpers', () => {
test('getClinePassUsageUrl returns the canonical usage endpoint', () => {
expect(getClinePassUsageUrl()).toBe('https://api.cline.bot/api/v1/users/me/plan/usage-limits')
})
})
describe('fetchClinePassUsage', () => {
const originalEnv = { ...process.env }
let originalFetch: typeof globalThis.fetch
const originalMacro = (globalThis as unknown as Record<string, unknown>).MACRO
beforeEach(() => {
process.env.CLINE_API_KEY = 'cp-test-key'
originalFetch = globalThis.fetch
;(globalThis as unknown as Record<string, unknown>).MACRO = { VERSION: 'test' }
})
afterEach(() => {
mock.restore()
globalThis.fetch = originalFetch
;(globalThis as unknown as Record<string, unknown>).MACRO = originalMacro
process.env = { ...originalEnv }
})
test('throws when CLINE_API_KEY is missing', async () => {
delete process.env.CLINE_API_KEY
await expect(fetchClinePassUsage()).rejects.toThrow(
'ClinePass auth is required. Set CLINE_API_KEY.',
)
})
test('sends Authorization bearer header and returns normalized usage', async () => {
const fetchMock = mock(async (_input: RequestInfo | URL, init?: RequestInit) => {
return new Response(
JSON.stringify({
data: {
limits: [
{ type: 'five_hour', percentUsed: 25, resetsAt: '2026-06-29T22:00:00.000Z' },
],
},
}),
{ status: 200 },
)
})
globalThis.fetch = fetchMock as unknown as typeof globalThis.fetch
const result = await fetchClinePassUsage()
expect(result.availability).toBe('available')
expect(result.windows).toEqual([
{ label: '5-Hour Limit', type: 'five_hour', usedPercent: 25, resetsAt: '2026-06-29T22:00:00.000Z' },
])
const [url, init] = fetchMock.mock.calls[0]
expect(url).toBe(getClinePassUsageUrl())
expect(init?.method).toBe('GET')
expect((init?.headers as Record<string, string>)?.Authorization).toBe('Bearer cp-test-key')
})
test('returns unknown availability on non-OK response', async () => {
globalThis.fetch = mock(async () => {
return new Response('Unauthorized', { status: 401 })
}) as unknown as typeof globalThis.fetch
const result = await fetchClinePassUsage()
expect(result.availability).toBe('unknown')
expect(result.windows).toEqual([])
})
test('propagates network errors', async () => {
globalThis.fetch = mock(async () => {
throw new Error('net failure')
}) as unknown as typeof globalThis.fetch
await expect(fetchClinePassUsage()).rejects.toThrow('net failure')
})
})
+16
View File
@@ -0,0 +1,16 @@
export type {
ClinePassUsageData,
ClinePassUsageRow,
ClinePassUsageWindow,
} from './clinepassUsage/types.js'
export {
buildClinePassUsageRows,
normalizeClinePassUsagePayload,
} from './clinepassUsage/parse.js'
export {
fetchClinePassUsage,
getClinePassUsageUrl,
resolveClinePassUsageBaseUrl,
} from './clinepassUsage/fetch.js'
+80
View File
@@ -0,0 +1,80 @@
import { createCombinedAbortSignal } from '../../../utils/combinedAbortSignal.js'
import { logForDebugging } from '../../../utils/debug.js'
import { getClaudeCodeUserAgent } from '../../../utils/userAgent.js'
import {
DEFAULT_CLINEPASS_BASE_URL,
DEFAULT_CLINEPASS_UNAVAILABLE_MESSAGE,
type ClinePassUsageData,
} from './types.js'
import { normalizeClinePassUsagePayload } from './parse.js'
function trimTrailingSlash(value: string): string {
return value.replace(/\/+$/, '')
}
export function resolveClinePassUsageBaseUrl(): string {
return DEFAULT_CLINEPASS_BASE_URL
}
function buildUnavailableResult(message: string): ClinePassUsageData {
return {
availability: 'unknown',
windows: [],
message,
}
}
export function getClinePassUsageUrl(): string {
const base = trimTrailingSlash(DEFAULT_CLINEPASS_BASE_URL)
return `${base}/api/v1/users/me/plan/usage-limits`
}
export async function fetchClinePassUsage(): Promise<ClinePassUsageData> {
const apiKey = process.env.CLINE_API_KEY
if (!apiKey) {
throw new Error(
'ClinePass auth is required. Set CLINE_API_KEY.',
)
}
const usageUrl = getClinePassUsageUrl()
let response: Response
const { signal, cleanup } = createCombinedAbortSignal(undefined, {
timeoutMs: 10000,
})
try {
try {
response = await fetch(usageUrl, {
method: 'GET',
headers: {
Accept: 'application/json',
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json',
'User-Agent': getClaudeCodeUserAgent(),
},
signal,
})
} catch (error) {
logForDebugging(
`[clinepass] usage request failed for ${usageUrl}: ${error instanceof Error ? error.message : String(error)}`,
{ level: 'warn' },
)
throw error instanceof Error ? error : new Error(String(error))
}
if (!response.ok) {
const errorBody = await response.text().catch(() => '')
logForDebugging(
`[clinepass] usage endpoint returned status ${response.status}: ${errorBody}`,
{ level: 'warn' },
)
return buildUnavailableResult(DEFAULT_CLINEPASS_UNAVAILABLE_MESSAGE)
}
const normalized = normalizeClinePassUsagePayload(await response.json())
return normalized
} finally {
cleanup()
}
}
+130
View File
@@ -0,0 +1,130 @@
import {
DEFAULT_CLINEPASS_UNAVAILABLE_MESSAGE,
type ClinePassUsageData,
type ClinePassUsageRow,
type ClinePassUsageWindow,
} from './types.js'
type RecordLike = Record<string, unknown>
function isRecord(value: unknown): value is RecordLike {
return typeof value === 'object' && value !== null
}
function asString(value: unknown): string | undefined {
return typeof value === 'string' && value.trim() ? value.trim() : undefined
}
function asNumber(value: unknown): number | undefined {
if (typeof value === 'number' && Number.isFinite(value)) {
return value
}
if (typeof value === 'string' && value.trim()) {
const parsed = Number.parseFloat(value)
if (Number.isFinite(parsed)) {
return parsed
}
}
return undefined
}
function clampPercent(value: number): number {
return Math.max(0, Math.min(100, Math.round(value)))
}
function toIsoDate(value: unknown): string | undefined {
if (typeof value === 'string') {
const parsed = Date.parse(value)
return Number.isNaN(parsed) ? undefined : new Date(parsed).toISOString()
}
return undefined
}
const WINDOW_TYPE_LABELS: Record<string, string> = {
five_hour: '5-Hour Limit',
weekly: 'Weekly Limit',
monthly: 'Monthly Limit',
}
function normalizeWindow(raw: unknown): ClinePassUsageWindow | undefined {
if (!isRecord(raw)) return undefined
const type = asString(raw.type) ?? 'unknown'
const percentUsed = asNumber(raw.percentUsed)
if (percentUsed === undefined) return undefined
const label = WINDOW_TYPE_LABELS[type] ?? type
const resetsAt = toIsoDate(raw.resetsAt)
return {
label,
type,
usedPercent: clampPercent(percentUsed),
resetsAt,
}
}
function normalizeLimitsArray(limits: unknown): ClinePassUsageWindow[] {
if (!Array.isArray(limits)) return []
return limits
.map(normalizeWindow)
.filter((w): w is ClinePassUsageWindow => w !== undefined)
}
export function normalizeClinePassUsagePayload(
payload: unknown,
): ClinePassUsageData {
if (!isRecord(payload)) {
return {
availability: 'unknown',
windows: [],
message: DEFAULT_CLINEPASS_UNAVAILABLE_MESSAGE,
}
}
const data = isRecord(payload.data) ? payload.data : payload
const limits =
isRecord(data) && Array.isArray(data.limits)
? data.limits
: []
const windows = normalizeLimitsArray(limits)
if (windows.length === 0) {
return {
availability: 'unknown',
windows: [],
message: DEFAULT_CLINEPASS_UNAVAILABLE_MESSAGE,
}
}
const planName = asString(
payload.planName ??
payload.plan_name ??
data.planName ??
data.plan_name,
)
return {
availability: 'available',
planName,
windows,
}
}
export function buildClinePassUsageRows(
windows: ClinePassUsageWindow[],
): ClinePassUsageRow[] {
const rows: ClinePassUsageRow[] = []
for (const window of windows) {
rows.push({
kind: 'window',
label: window.label,
usedPercent: window.usedPercent,
resetsAt: window.resetsAt,
})
}
return rows
}
+30
View File
@@ -0,0 +1,30 @@
export type ClinePassUsageWindow = {
label: string
type: string
usedPercent: number
resetsAt?: string
}
export type ClinePassUsageData =
| {
availability: 'available'
planName?: string
windows: ClinePassUsageWindow[]
}
| {
availability: 'unknown'
planName?: string
windows: ClinePassUsageWindow[]
message: string
}
export type ClinePassUsageRow = {
kind: 'window'
label: string
usedPercent: number
resetsAt?: string
}
export const DEFAULT_CLINEPASS_BASE_URL = 'https://api.cline.bot'
export const DEFAULT_CLINEPASS_UNAVAILABLE_MESSAGE =
'Usage details are not available for this ClinePass account.'
+113
View File
@@ -102,3 +102,116 @@ test('resolveProviderRequest leaves Hicap routing untouched without explicit ali
expect(request.resolvedModel).toBe('claude-opus-4-7')
expect(request.baseUrl).toBe('https://api.hicap.ai/v1')
})
test('resolveProviderRequest uses CLINE_API_MODEL when CLINE_API_KEY is present', () => {
const request = resolveProviderRequest({
processEnv: {
CLINE_API_KEY: 'cp-key',
CLINE_API_MODEL: 'cline-pass/qwen3.7-max',
},
})
expect(request.requestedModel).toBe('cline-pass/qwen3.7-max')
expect(request.baseUrl).toBe('https://api.cline.bot/api/v1')
})
test('resolveProviderRequest falls back to OPENAI_MODEL for ClinePass when CLINE_API_MODEL is unset', () => {
const request = resolveProviderRequest({
processEnv: {
CLINE_API_KEY: 'cp-key',
OPENAI_MODEL: 'cline-pass/deepseek-v4-flash',
},
})
expect(request.requestedModel).toBe('cline-pass/deepseek-v4-flash')
expect(request.baseUrl).toBe('https://api.cline.bot/api/v1')
})
test('resolveProviderRequest treats blank CLINE_API_MODEL as unset for ClinePass', () => {
const request = resolveProviderRequest({
processEnv: {
CLINE_API_KEY: 'cp-key',
CLINE_API_MODEL: ' ',
OPENAI_MODEL: 'cline-pass/qwen3.7-max',
},
})
expect(request.requestedModel).toBe('cline-pass/qwen3.7-max')
expect(request.baseUrl).toBe('https://api.cline.bot/api/v1')
})
test('resolveProviderRequest uses the ClinePass route default when no model env is set', () => {
const request = resolveProviderRequest({
processEnv: {
CLINE_API_KEY: 'cp-key',
},
})
expect(request.requestedModel).toBe('cline-pass/deepseek-v4-flash')
expect(request.baseUrl).toBe('https://api.cline.bot/api/v1')
})
test('resolveProviderRequest ignores CLINE_API_MODEL without CLINE_API_KEY', () => {
const request = resolveProviderRequest({
processEnv: {
CLINE_API_MODEL: 'cline-pass/qwen3.7-max',
OPENAI_API_KEY: 'openai-key',
OPENAI_MODEL: 'gpt-4o',
},
})
expect(request.requestedModel).toBe('gpt-4o')
expect(request.baseUrl).toBe('https://api.openai.com/v1')
})
test('resolveProviderRequest ignores ClinePass model when GitHub mode is active', () => {
const request = resolveProviderRequest({
processEnv: {
CLAUDE_CODE_USE_GITHUB: '1',
CLINE_API_KEY: 'cp-key',
CLINE_API_MODEL: 'cline-pass/qwen3.7-max',
},
})
expect(request.requestedModel).toBe('github:copilot')
expect(request.baseUrl).not.toContain('cline.bot')
})
test('resolveProviderRequest ignores ClinePass model when explicit OPENAI_BASE_URL points elsewhere', () => {
const request = resolveProviderRequest({
processEnv: {
CLINE_API_KEY: 'cp-key',
CLINE_API_MODEL: 'cline-pass/qwen3.7-max',
OPENAI_BASE_URL: 'https://api.openai.com/v1',
OPENAI_MODEL: 'gpt-4o',
},
})
expect(request.requestedModel).toBe('gpt-4o')
expect(request.baseUrl).toBe('https://api.openai.com/v1')
})
test('resolveProviderRequest ignores ClinePass model when explicit baseUrl option points elsewhere', () => {
const request = resolveProviderRequest({
baseUrl: 'https://openrouter.ai/api/v1',
processEnv: {
CLINE_API_KEY: 'cp-key',
CLINE_API_MODEL: 'cline-pass/qwen3.7-max',
},
})
expect(request.requestedModel).toBe('codexplan')
expect(request.baseUrl).toBe('https://openrouter.ai/api/v1')
})
test('resolveProviderRequest uses ClinePass model when no explicit base URL is set', () => {
const request = resolveProviderRequest({
processEnv: {
CLINE_API_KEY: 'cp-key',
CLINE_API_MODEL: 'cline-pass/qwen3.7-max',
},
})
expect(request.requestedModel).toBe('cline-pass/qwen3.7-max')
expect(request.baseUrl).toBe('https://api.cline.bot/api/v1')
})
+41 -13
View File
@@ -19,7 +19,14 @@ import {
DEFAULT_GEMINI_BASE_URL,
DEFAULT_GEMINI_MODEL,
} from 'src/utils/providerProfile.js'
import {
DEFAULT_CLINEPASS_BASE_URL,
} from './clinepassUsage/types.js'
import { getCatalogEntriesForRoute } from '../../integrations/registry.js'
import {
getRouteDefaultModel,
isClinePassBaseUrl,
} from '../../integrations/routeMetadata.js'
import {
openAIShimSupportsApiFormatForModel,
resolveOpenAIShimRuntimeContext,
@@ -30,6 +37,7 @@ export const DEFAULT_CODEX_BASE_URL = 'https://chatgpt.com/backend-api/codex'
export const DEFAULT_MISTRAL_BASE_URL = 'https://api.mistral.ai/v1'
export const DEFAULT_OPENCODE_BASE_URL = 'https://opencode.ai/zen/v1'
export const DEFAULT_OPENCODE_GO_BASE_URL = 'https://opencode.ai/zen/go/v1'
export const DEFAULT_CLINEPASS_API_BASE_URL = `${DEFAULT_CLINEPASS_BASE_URL}/api/v1`
/** Default GitHub Copilot API model when user selects copilot / github:copilot */
export const DEFAULT_GITHUB_MODELS_API_MODEL = 'gpt-4o'
const warnedUndefinedEnvNames = new Set<string>()
@@ -797,18 +805,7 @@ export function resolveProviderRequest(options?: {
const isGithubMode = isEnvTruthy(processEnv.CLAUDE_CODE_USE_GITHUB)
const isMistralMode = isEnvTruthy(processEnv.CLAUDE_CODE_USE_MISTRAL)
const isGeminiMode = isEnvTruthy(processEnv.CLAUDE_CODE_USE_GEMINI)
const requestedModel =
options?.model?.trim() ||
(isMistralMode
? processEnv.MISTRAL_MODEL?.trim()
: processEnv.OPENAI_MODEL?.trim()) ||
(isGeminiMode
? processEnv.GEMINI_MODEL?.trim()
: processEnv.OPENAI_MODEL?.trim()) ||
options?.fallbackModel?.trim() ||
(isGeminiMode ? DEFAULT_GEMINI_MODEL : undefined) ||
(isGithubMode ? 'github:copilot' : 'codexplan')
const descriptor = parseModelDescriptor(requestedModel)
const isClinePassMode = Boolean(processEnv.CLINE_API_KEY?.trim())
const explicitBaseUrl = asEnvUrl(options?.baseUrl)
const normalizedMistralEnvBaseUrl = asNamedEnvUrl(
@@ -842,10 +839,41 @@ export function resolveProviderRequest(options?: {
? asNamedEnvUrl(processEnv.OPENAI_API_BASE, 'OPENAI_API_BASE')
: undefined)
// ClinePass model selection is only valid when no concrete non-ClinePass
// base URL is explicitly provided via options or env. This prevents stale
// CLINE_API_KEY/CLINE_API_MODEL from overriding an explicit OPENAI_BASE_URL
// pointing at a different provider.
const concreteBaseUrlBeforeDefault =
explicitBaseUrl ?? primaryEnvBaseUrl ?? fallbackEnvBaseUrl
const hasConcreteNonClinePassBaseUrl =
Boolean(concreteBaseUrlBeforeDefault) && !isClinePassBaseUrl(concreteBaseUrlBeforeDefault)
const effectiveClinePassMode =
isClinePassMode && !isGithubMode && !hasConcreteNonClinePassBaseUrl
const clinePassDefaultModel = effectiveClinePassMode
? getRouteDefaultModel('clinepass')
: undefined
const requestedModel =
options?.model?.trim() ||
(isMistralMode
? processEnv.MISTRAL_MODEL?.trim()
: isGeminiMode
? processEnv.GEMINI_MODEL?.trim()
: effectiveClinePassMode
? processEnv.CLINE_API_MODEL?.trim() ||
processEnv.OPENAI_MODEL?.trim()
: processEnv.OPENAI_MODEL?.trim()) ||
options?.fallbackModel?.trim() ||
(isGeminiMode ? DEFAULT_GEMINI_MODEL : undefined) ||
clinePassDefaultModel ||
(isGithubMode ? 'github:copilot' : 'codexplan')
const descriptor = parseModelDescriptor(requestedModel)
const envBaseUrlRaw =
explicitBaseUrl ??
primaryEnvBaseUrl ??
fallbackEnvBaseUrl
fallbackEnvBaseUrl ??
(effectiveClinePassMode ? DEFAULT_CLINEPASS_API_BASE_URL : undefined)
const githubEnterpriseEnvUrl = asGithubEnterpriseEnvUrl(
processEnv.GITHUB_ENTERPRISE_URL,
+3
View File
@@ -106,6 +106,7 @@ const PROFILE_ENV_KEYS = [
'ATLAS_CLOUD_API_KEY',
'NEARAI_API_KEY',
'FIREWORKS_API_KEY',
'CLINE_API_KEY',
'OPENCODE_API_KEY',
DEFAULT_STARTUP_PROVIDER_ENV_VAR,
] as const
@@ -136,6 +137,7 @@ export type ProviderProfile =
| 'vertex'
| 'xai'
| 'opencode'
| 'clinepass'
export type ProfileEnv = {
ANTHROPIC_BASE_URL?: string
@@ -181,6 +183,7 @@ export type ProfileEnv = {
VENICE_API_KEY?: string
MIMO_API_KEY?: string
ATLAS_CLOUD_API_KEY?: string
CLINE_API_KEY?: string
NEARAI_API_KEY?: string
FIREWORKS_API_KEY?: string
OPENCODE_API_KEY?: string
+158
View File
@@ -65,6 +65,7 @@ const RESTORED_KEYS = [
'VENICE_API_KEY',
'MIMO_API_KEY',
'ATLAS_CLOUD_API_KEY',
'CLINE_API_KEY',
'HICAP_API_KEY',
'CLAUDE_CODE_OPENAI_CONTEXT_WINDOWS',
] as const
@@ -257,6 +258,17 @@ function buildAtlasCloudProfile(overrides: Partial<ProviderProfile> = {}): Provi
})
}
function buildClinePassProfile(overrides: Partial<ProviderProfile> = {}): ProviderProfile {
return buildProfile({
provider: 'clinepass',
name: 'ClinePass',
baseUrl: 'https://api.cline.bot/api/v1',
model: 'cline-pass/deepseek-v4-flash',
apiKey: 'cline-test-key',
...overrides,
})
}
describe('applyProviderProfileToProcessEnv', () => {
test('openai profile clears competing gemini/github flags', async () => {
const { applyProviderProfileToProcessEnv } =
@@ -828,6 +840,64 @@ describe('applyProviderProfileToProcessEnv', () => {
expect(getFreshAPIProvider()).toBe('openai')
})
test('ClinePass preset profile applies OpenAI-compatible env with CLINE_API_KEY mirror', async () => {
const { applyProviderProfileToProcessEnv } =
await importFreshProviderProfileModules()
process.env.CLAUDE_CODE_USE_GEMINI = '1'
applyProviderProfileToProcessEnv(buildClinePassProfile())
const { getAPIProvider: getFreshAPIProvider } =
await importFreshProvidersModule()
expect(process.env.CLAUDE_CODE_USE_GEMINI).toBeUndefined()
expect(String(process.env.CLAUDE_CODE_USE_OPENAI)).toBe('1')
expect(process.env.OPENAI_BASE_URL).toBe('https://api.cline.bot/api/v1')
expect(process.env.OPENAI_MODEL).toBe('cline-pass/deepseek-v4-flash')
expect(process.env.OPENAI_API_KEY).toBe('cline-test-key')
expect(process.env.CLINE_API_KEY).toBe('cline-test-key')
expect(getFreshAPIProvider()).toBe('openai')
})
test('custom openai profile targeting ClinePass base URL applies CLINE_API_KEY mirror', async () => {
const { applyProviderProfileToProcessEnv } =
await importFreshProviderProfileModules()
applyProviderProfileToProcessEnv(
buildProfile({
provider: 'openai',
name: 'Custom ClinePass',
baseUrl: 'https://api.cline.bot/api/v1',
model: 'cline-pass/qwen3.7-max',
apiKey: 'custom-cline-key',
}),
)
expect(process.env.OPENAI_BASE_URL).toBe('https://api.cline.bot/api/v1')
expect(process.env.OPENAI_MODEL).toBe('cline-pass/qwen3.7-max')
expect(process.env.OPENAI_API_KEY).toBe('custom-cline-key')
expect(process.env.CLINE_API_KEY).toBe('custom-cline-key')
})
test('ClinePass provider with custom base URL applies CLINE_API_KEY mirror', async () => {
const { applyProviderProfileToProcessEnv } =
await importFreshProviderProfileModules()
applyProviderProfileToProcessEnv(
buildProfile({
provider: 'clinepass',
name: 'ClinePass Custom',
baseUrl: 'https://custom.cline.bot/v1',
model: 'cline-pass/qwen3.7-max',
apiKey: 'custom-cline-key',
}),
)
expect(process.env.OPENAI_BASE_URL).toBe('https://custom.cline.bot/v1')
expect(process.env.OPENAI_MODEL).toBe('cline-pass/qwen3.7-max')
expect(process.env.OPENAI_API_KEY).toBe('custom-cline-key')
expect(process.env.CLINE_API_KEY).toBe('custom-cline-key')
})
test('legacy OpenAI profile on restricted route ignores advanced settings', async () => {
const { applyProviderProfileToProcessEnv } =
await importFreshProviderProfileModules()
@@ -2203,6 +2273,94 @@ describe('setActiveProviderProfile', () => {
}
})
test('persists ClinePass profiles using a legacy-compatible openai startup profile', 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 clinePassProfile = buildClinePassProfile({
id: 'clinepass_prof',
model: 'cline-pass/deepseek-v4-flash, cline-pass/qwen3.7-max',
})
saveMockGlobalConfig(current => ({
...current,
providerProfiles: [clinePassProfile],
}))
const result = setActiveProviderProfile('clinepass_prof', {
configDir,
})
const persisted = JSON.parse(
readFileSync(join(configDir, '.openclaude-profile.json'), 'utf8'),
)
expect(result?.id).toBe('clinepass_prof')
expect(existsSync(join(tempDir, '.openclaude-profile.json'))).toBe(false)
expect(persisted.profile).toBe('openai')
expect(persisted.env).toEqual({
OPENAI_BASE_URL: 'https://api.cline.bot/api/v1',
OPENAI_MODEL: 'cline-pass/deepseek-v4-flash',
OPENAI_API_KEY: 'cline-test-key',
CLINE_API_KEY: 'cline-test-key',
})
} finally {
process.chdir(originalCwd)
rmSync(tempDir, { recursive: true, force: true })
rmSync(configDir, { recursive: true, force: true })
}
})
test('persists ClinePass profiles with custom base URL using the same dedicated credential', 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 clinePassProfile = buildProfile({
provider: 'clinepass',
name: 'ClinePass Custom',
baseUrl: 'https://custom.cline.bot/v1',
model: 'cline-pass/qwen3.7-max',
apiKey: 'custom-cline-key',
id: 'clinepass_custom',
})
saveMockGlobalConfig(current => ({
...current,
providerProfiles: [clinePassProfile],
}))
const result = setActiveProviderProfile('clinepass_custom', {
configDir,
})
const persisted = JSON.parse(
readFileSync(join(configDir, '.openclaude-profile.json'), 'utf8'),
)
expect(result?.id).toBe('clinepass_custom')
expect(existsSync(join(tempDir, '.openclaude-profile.json'))).toBe(false)
expect(persisted.profile).toBe('openai')
expect(persisted.env).toEqual({
OPENAI_BASE_URL: 'https://custom.cline.bot/v1',
OPENAI_MODEL: 'cline-pass/qwen3.7-max',
OPENAI_API_KEY: 'custom-cline-key',
CLINE_API_KEY: 'custom-cline-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-'))
+47 -28
View File
@@ -46,6 +46,7 @@ import {
type ProviderPreset,
} from '../integrations/index.js'
import {
isClinePassBaseUrl,
isFireworksBaseUrl,
isNearaiBaseUrl,
isXaiBaseUrl,
@@ -136,6 +137,11 @@ function resolveProfileCompatibility(provider: string): {
return { route, compatibilityMode: 'openai' }
}
function isClinePassProfile(profile: ProviderProfile): boolean {
const { route } = resolveProfileCompatibility(profile.provider)
return route.routeId === 'clinepass' || isClinePassBaseUrl(profile.baseUrl)
}
function deriveGithubEnterpriseUrl(baseUrl: string | undefined): string | undefined {
if (!baseUrl?.trim()) return undefined
try {
@@ -696,6 +702,10 @@ function isProcessEnvAlignedWithProfile(
? !includeApiKey ||
sameOptionalEnvValue(processEnv.ATLAS_CLOUD_API_KEY, profile.apiKey)
: true) &&
(isClinePassProfile(profile)
? !includeApiKey ||
sameOptionalEnvValue(processEnv.CLINE_API_KEY, profile.apiKey)
: true) &&
(isNearaiBaseUrl(profile.baseUrl)
? !includeApiKey ||
sameOptionalEnvValue(processEnv.NEARAI_API_KEY, profile.apiKey)
@@ -853,6 +863,9 @@ export function applyProviderProfileToProcessEnv(
if (route.routeId === 'atlas-cloud' || profile.baseUrl.toLowerCase().includes('atlascloud')) {
openAIProfileEnv.ATLAS_CLOUD_API_KEY = profile.apiKey
}
if (isClinePassProfile(profile)) {
openAIProfileEnv.CLINE_API_KEY = profile.apiKey
}
if (route.routeId === 'nearai' || isNearaiBaseUrl(profile.baseUrl)) {
openAIProfileEnv.NEARAI_API_KEY = profile.apiKey
}
@@ -1118,6 +1131,9 @@ function buildOpenAICompatibleStartupEnv(
if (activeProfile.baseUrl?.toLowerCase().includes('atlascloud')) {
strictEnv.ATLAS_CLOUD_API_KEY = activeProfile.apiKey
}
if (isClinePassProfile(activeProfile)) {
strictEnv.CLINE_API_KEY = activeProfile.apiKey
}
if (isNearaiBaseUrl(activeProfile.baseUrl)) {
strictEnv.NEARAI_API_KEY = activeProfile.apiKey
}
@@ -1144,35 +1160,38 @@ function buildOpenAICompatibleStartupEnv(
: {}),
}
if (activeProfile.apiKey) {
env.OPENAI_API_KEY = activeProfile.apiKey
if (activeProfile.baseUrl?.toLowerCase().includes('bankr')) {
env.BNKR_API_KEY = activeProfile.apiKey
if (activeProfile.apiKey) {
env.OPENAI_API_KEY = activeProfile.apiKey
if (activeProfile.baseUrl?.toLowerCase().includes('bankr')) {
env.BNKR_API_KEY = activeProfile.apiKey
}
if (isXaiBaseUrl(activeProfile.baseUrl)) {
env.XAI_API_KEY = activeProfile.apiKey
}
if (activeProfile.baseUrl?.toLowerCase().includes('api.venice.ai')) {
env.VENICE_API_KEY = activeProfile.apiKey
}
if (
activeProfile.baseUrl?.toLowerCase().includes('api.xiaomimimo.com') ||
activeProfile.baseUrl?.toLowerCase().includes('api.mimo-v2.com')
) {
env.MIMO_API_KEY = activeProfile.apiKey
}
if (activeProfile.baseUrl?.toLowerCase().includes('atlascloud')) {
env.ATLAS_CLOUD_API_KEY = activeProfile.apiKey
}
if (isClinePassProfile(activeProfile)) {
env.CLINE_API_KEY = activeProfile.apiKey
}
if (isNearaiBaseUrl(activeProfile.baseUrl)) {
env.NEARAI_API_KEY = activeProfile.apiKey
}
if (isFireworksBaseUrl(activeProfile.baseUrl)) {
env.FIREWORKS_API_KEY = activeProfile.apiKey
}
} else {
delete env.OPENAI_API_KEY
}
if (isXaiBaseUrl(activeProfile.baseUrl)) {
env.XAI_API_KEY = activeProfile.apiKey
}
if (activeProfile.baseUrl?.toLowerCase().includes('api.venice.ai')) {
env.VENICE_API_KEY = activeProfile.apiKey
}
if (
activeProfile.baseUrl?.toLowerCase().includes('api.xiaomimimo.com') ||
activeProfile.baseUrl?.toLowerCase().includes('api.mimo-v2.com')
) {
env.MIMO_API_KEY = activeProfile.apiKey
}
if (activeProfile.baseUrl?.toLowerCase().includes('atlascloud')) {
env.ATLAS_CLOUD_API_KEY = activeProfile.apiKey
}
if (isNearaiBaseUrl(activeProfile.baseUrl)) {
env.NEARAI_API_KEY = activeProfile.apiKey
}
if (isFireworksBaseUrl(activeProfile.baseUrl)) {
env.FIREWORKS_API_KEY = activeProfile.apiKey
}
} else {
delete env.OPENAI_API_KEY
}
return applySupportedProfileCustomHeaders(activeProfile, env)
}