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
7 changes: 6 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -192,8 +192,13 @@ ANTHROPIC_API_KEY=sk-ant-your-key-here
# For Z.AI GLM Coding Plan, set:
# OPENAI_BASE_URL=https://api.z.ai/api/coding/paas/v4
# OPENAI_MODEL=glm-5.2
# Optional: OPENAI_MODEL=glm-5.3 (the default remains glm-5.2)
# Optional: OPENAI_MODEL=glm-5.3-flash (supports image input; the default remains glm-5.2)
# Optional: OPENAI_MODEL=glm-5.3
# Optional: OPENAI_MODEL=GLM-5.1, GLM-5-Turbo, GLM-4.7, or GLM-4.5-Air
# Optional GLM-5.3-Flash thinking controls:
# OPENAI_MODEL='glm-5.3-flash?reasoning=low' # requests Z.AI reasoning_effort=low
# OPENAI_MODEL='glm-5.3-flash?reasoning=high' # requests Z.AI reasoning_effort=high
# OPENAI_MODEL='glm-5.3-flash?reasoning=xhigh' # maps to Z.AI reasoning_effort=max
# Optional GLM-5.3 thinking controls:
# OPENAI_MODEL='glm-5.3?reasoning=low' # requests Z.AI reasoning_effort=low
# OPENAI_MODEL='glm-5.3?reasoning=high' # requests Z.AI reasoning_effort=high
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,7 @@ Advanced and source-build guides:
| Provider | Setup Path | Notes |
| --- | --- | --- |
| OpenAI-compatible | `/provider` or env vars | Works with OpenAI, OpenRouter, DeepSeek, Groq, Mistral, LM Studio, and other compatible `/v1` servers |
| 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` |
| Z.AI GLM Coding Plan | `/provider` or OpenAI-compatible env vars | Uses `OPENAI_API_KEY` at `https://api.z.ai/api/coding/paas/v4`, defaults to `glm-5.2`, and offers the vision-capable `glm-5.3-flash` option |
| AI/ML API | `/provider` or `AIMLAPI_API_KEY` ([setup guide](docs/aimlapi-setup.md)) | Uses `https://api.aimlapi.com/v1`, auto-detects the OpenAI-compatible route from `AIMLAPI_API_KEY`, sends OpenClaude attribution headers, and discovers chat-capable models from the public `/models` catalog |
| Concentrate | `/provider` or `CONCENTRATE_API_KEY` | Unified OpenAI-compatible gateway at `https://api.concentrate.ai/v1`; defaults to `deepseek-v4-flash` and auto-discovers the chat model catalog |
| LLMTR | `/provider` or OpenAI-compatible env vars | Multi-model gateway at `https://llmtr.com/v1`; `/provider` and `--provider llmtr` default to `deepseek/deepseek-v4-flash`, while raw env setup must set `OPENAI_BASE_URL=https://llmtr.com/v1` and `OPENAI_MODEL`; accepts `LLMTR_API_KEY` or `OPENAI_API_KEY` after the route is selected and discovers tool-capable Chat Completions models from the public catalog |
Expand Down Expand Up @@ -373,7 +373,7 @@ OpenClaude supports multiple providers, but behavior is not identical across all
- Some providers impose lower output caps than the CLI defaults, and OpenClaude adapts where possible
- AI/ML API uses the OpenAI-compatible route, defaults to `gpt-4o`, and only surfaces chat-capable models from its public catalog
- Gitlawb Opengateway is the fresh-install startup default and requires an API key from https://gitlawb.com/opengateway/keys. It uses one OpenAI-compatible base URL; switch between `mimo-*` and `google/gemini-3.1-flash-lite-preview` with `/model`, and do not pin the base URL to `/v1/xiaomi-mimo`.
- Z.AI GLM Coding Plan uses `https://api.z.ai/api/coding/paas/v4` with `glm-5.2` by default. GLM-5.3 is selectable as `glm-5.3`; use `glm-5.3?reasoning=low`, `glm-5.3?reasoning=high`, or `glm-5.3?reasoning=xhigh` to request its documented low, high, or maximum effort. The existing GLM-5.2 query controls remain supported.
- Z.AI GLM Coding Plan uses `https://api.z.ai/api/coding/paas/v4` with `glm-5.2` by default. On that direct route, GLM-5.3-Flash is selectable as `glm-5.3-flash` and accepts image input. Its OpenClaude effort choices are `low`, `high`, and `xhigh`, with `xhigh` requesting Z.AI `max`; GLM-5.3 and the existing GLM-5.2 query controls remain supported. These capabilities are not claimed for gateways that happen to carry the same model name.
- Xiaomi MiMo uses `api-key` header auth on the direct OpenAI-compatible route and currently does not support `/usage` reporting in OpenClaude
- GitHub Copilot serializes sub-agent execution by default to reduce Premium Request consumption — see [Agent Routing and Step Limits](docs/agent-routing.md#github-copilot-sub-agent-optimization) for tuning

Expand Down
4 changes: 3 additions & 1 deletion docs/integrations/reasoning-effort.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,9 @@ Use `mode: 'always-on'` with `wireFormat: 'none'` for models that emit reasoning

Currently wired metadata formats are `reasoning_effort`, `deepseek_compatible`, and `zai_compatible`. The descriptor type also reserves `reasoning_object` and `thinking_type`, but those formats are not request-plumbed yet and should not be used to enable `/effort`.

For `deepseek_compatible` and `zai_compatible`, metadata levels must be limited to `high` and/or `xhigh`. These serializers emit provider `high` for `high` and provider `max` for `xhigh`; they cannot faithfully represent `low`, `medium`, or standard `max` as distinct UI levels.
For `deepseek_compatible`, metadata levels must be limited to `high` and/or `xhigh`. The serializer emits provider `high` for OpenClaude `high` and provider `max` for OpenClaude `xhigh`; it cannot represent `low`, `medium`, or standard `max` as distinct levels.

For `zai_compatible`, exact route/model entries may expose verified `low`, `high`, and `xhigh` levels. These serialize as provider `low`, `high`, and `max`, respectively. `medium` is not a distinct GLM-5.3-Flash level, and standard OpenClaude `max` is not added as another picker level. Disabling thinking must be verified separately; low effort is not equivalent to no reasoning.

## Adding Support

Expand Down
50 changes: 50 additions & 0 deletions src/commands/usage/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,56 @@ describe('getUsageDescriptor', () => {
).toBe('openrouter')
})

test('resolveActiveUsageId prefers explicit runtime URLs over a saved Z.AI profile URL', () => {
const profile = {
activeProfileProvider: 'zai',
activeProfileBaseUrl: 'https://api.z.ai/api/coding/paas/v4',
providerCategory: 'openai',
}

expect(
resolveActiveUsageId(
{
CLAUDE_CODE_USE_OPENAI: '1',
OPENAI_BASE_URL: 'https://api.z.ai/api/paas/v4',
} as NodeJS.ProcessEnv,
profile,
),
).toBe('custom')
expect(
resolveActiveUsageId(
{
CLAUDE_CODE_USE_OPENAI: '1',
OPENAI_BASE_URL: 'https://proxy.example.test/v1',
} as NodeJS.ProcessEnv,
profile,
),
).toBe('custom')
expect(
resolveActiveUsageId(
{
CLAUDE_CODE_USE_OPENAI: '1',
OPENAI_BASE_URL: profile.activeProfileBaseUrl,
} as NodeJS.ProcessEnv,
profile,
),
).toBe('zai')

expect(
resolveActiveUsageId(
{
CLAUDE_CODE_USE_OPENAI: '1',
OPENAI_BASE_URL: 'https://proxy.example.test/v1',
} as NodeJS.ProcessEnv,
{
activeProfileProvider: 'clinepass',
activeProfileBaseUrl: 'https://api.cline.bot/api/v1',
providerCategory: 'openai',
},
),
).toBe('custom')
})

test('resolves first-party usage support through the Anthropic vendor descriptor', () => {
const descriptor = getUsageDescriptor('firstParty')

Expand Down
1 change: 1 addition & 0 deletions src/integrations/brands/glm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export default defineBrand({
supportsPreciseTokenCount: false,
},
modelIds: [
'glm-5.3-flash',
'glm-5.3',
'glm-5.2',
'GLM-5.1',
Expand Down
15 changes: 15 additions & 0 deletions src/integrations/models/glm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,21 @@ function glmModel(
}

export default [
defineModel({
id: 'glm-5.3-flash',
label: 'GLM 5.3 Flash',
brandId: 'glm',
vendorId: 'zai',
classification: ['chat', 'reasoning', 'vision', 'coding'],
defaultModel: 'glm-5.3-flash',
capabilities: {
...glmCapabilities,
supportsVision: true,
},
contextWindow: 1_000_000,
maxOutputTokens: 131_072,
runtimeMetadataScope: 'catalog',
}),
glmModel('glm-5.3', 'GLM 5.3', 1_000_000, 131_072, 'catalog'),
defineModel({
id: 'glm-5v-turbo',
Expand Down
84 changes: 84 additions & 0 deletions src/integrations/routeMetadata.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,20 @@ test('custom Anthropic credentials stay native and resolve to their proxy route'
ANTHROPIC_AUTH_TOKEN: 'tenant-token',
}),
).toBe('custom-anthropic')

expect(
resolveActiveRouteIdFromEnv(
{
ANTHROPIC_BASE_URL: 'https://api.anthropic.com',
ANTHROPIC_MODEL: 'claude-sonnet-4-6',
ANTHROPIC_API_KEY: 'first-party-key',
},
{
activeProfileProvider: 'zai',
activeProfileBaseUrl: 'https://api.z.ai/api/coding/paas/v4',
},
),
).toBe('anthropic')
})

test('getRouteCredentialEnvVars omits the openai fallback for dedicatedCredentialsOnly routes', () => {
Expand Down Expand Up @@ -345,6 +359,64 @@ test('Venice route metadata uses official OpenAI-compatible defaults', () => {
expect(resolveRouteIdFromBaseUrl('https://api.venice.ai/api/v1/chat/completions')).toBe('venice')
})

test('Z.AI route metadata is limited to the canonical Coding Plan endpoint', () => {
expect(
resolveRouteIdFromBaseUrl('https://api.z.ai/api/coding/paas/v4'),
).toBe('zai')
expect(
resolveRouteIdFromBaseUrl('https://api.z.ai/api/paas/v4'),
).toBeNull()
})

test('active Z.AI profiles honor the Coding Plan endpoint boundary', () => {
const codingPlanUrl = 'https://api.z.ai/api/coding/paas/v4'
const generalUrl = 'https://api.z.ai/api/paas/v4'
const customUrl = 'https://proxy.example.test/v1'

expect(
resolveActiveRouteIdFromEnv(
{ CLAUDE_CODE_USE_OPENAI: '1', OPENAI_BASE_URL: codingPlanUrl },
{ activeProfileProvider: 'zai', activeProfileBaseUrl: codingPlanUrl },
),
).toBe('zai')
expect(
resolveActiveRouteIdFromEnv(
{ CLAUDE_CODE_USE_OPENAI: '1', OPENAI_BASE_URL: generalUrl },
{ activeProfileProvider: 'zai', activeProfileBaseUrl: generalUrl },
),
).toBe('custom')
for (const invalidPrimary of [' ', 'undefined', 'null']) {
expect(
resolveActiveRouteIdFromEnv(
{
CLAUDE_CODE_USE_OPENAI: '1',
OPENAI_BASE_URL: invalidPrimary,
OPENAI_API_BASE: generalUrl,
},
{ activeProfileProvider: 'zai', activeProfileBaseUrl: codingPlanUrl },
),
).toBe('custom')
}
expect(
resolveActiveRouteIdFromEnv(
{ CLAUDE_CODE_USE_OPENAI: '1', OPENAI_BASE_URL: generalUrl },
{ activeProfileProvider: 'zai', activeProfileBaseUrl: codingPlanUrl },
),
).toBe('custom')
expect(
resolveActiveRouteIdFromEnv(
{ CLAUDE_CODE_USE_OPENAI: '1', OPENAI_BASE_URL: customUrl },
{ activeProfileProvider: 'zai', activeProfileBaseUrl: codingPlanUrl },
),
).toBe('custom')
expect(
resolveActiveRouteIdFromEnv(
{ CLAUDE_CODE_USE_OPENAI: '1' },
{ activeProfileProvider: 'zai' },
),
).toBe('zai')
})

test('AI/ML API route metadata uses official OpenAI-compatible defaults', () => {
expect(getRouteDefaultBaseUrl('aimlapi')).toBe('https://api.aimlapi.com/v1')
expect(getRouteDefaultModel('aimlapi')).toBe('gpt-4o')
Expand Down Expand Up @@ -900,6 +972,18 @@ test('resolveActiveRouteIdFromEnv lets explicit OPENAI_BASE_URL override saved C
},
),
).toBe('openrouter')
expect(
resolveActiveRouteIdFromEnv(
{
CLAUDE_CODE_USE_OPENAI: '1',
OPENAI_BASE_URL: 'https://proxy.example.test/v1',
},
{
activeProfileProvider: 'clinepass',
activeProfileBaseUrl: 'https://api.cline.bot/api/v1',
},
),
).toBe('custom')
})

test('resolveActiveRouteIdFromEnv does not infer MiniMax with OpenAI credentials', () => {
Expand Down
35 changes: 27 additions & 8 deletions src/integrations/routeMetadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,17 @@ export function normalizeComparableBaseUrl(
}
}

const ZAI_CODING_PLAN_BASE_URL = 'https://api.z.ai/api/coding/paas/v4'

export function isCanonicalZaiCodingPlanBaseUrl(
value: string | undefined,
): boolean {
return (
normalizeComparableBaseUrl(value) ===
normalizeComparableBaseUrl(ZAI_CODING_PLAN_BASE_URL)
)
}

function normalizeHost(
baseUrl?: string,
): string | null {
Expand Down Expand Up @@ -1332,6 +1343,9 @@ function profileRouteHonorsBaseUrlBoundary(
if (routeId === 'llmtr') {
return isCanonicalLlmtrInferenceBaseUrl(baseUrl)
}
if (routeId === 'zai') {
return !baseUrl || isCanonicalZaiCodingPlanBaseUrl(baseUrl)
}
return true
}

Expand Down Expand Up @@ -1361,12 +1375,13 @@ export function resolveActiveRouteIdFromEnv(
// A Bearer token explicitly selects the custom Anthropic proxy contract,
// even if the host also belongs to a known OpenAI-compatible route. Keep
// native x-api-key configurations on those known routes for compatibility.
const knownAnthropicRoute = resolveRouteIdFromBaseUrl(
processEnv.ANTHROPIC_BASE_URL,
)
const anthropicBaseUrl = hasNonEmptyEnvValue(processEnv.ANTHROPIC_BASE_URL)
? processEnv.ANTHROPIC_BASE_URL
: undefined
const knownAnthropicRoute = resolveRouteIdFromBaseUrl(anthropicBaseUrl)
if (
!isEnvTruthy(processEnv.CLAUDE_CODE_USE_OPENAI) &&
hasNonEmptyEnvValue(processEnv.ANTHROPIC_BASE_URL) &&
anthropicBaseUrl &&
hasNonEmptyEnvValue(processEnv.ANTHROPIC_MODEL) &&
(hasNonEmptyEnvValue(processEnv.ANTHROPIC_AUTH_TOKEN) ||
hasNonEmptyEnvValue(processEnv.ANTHROPIC_API_KEY)) &&
Expand All @@ -1382,15 +1397,19 @@ export function resolveActiveRouteIdFromEnv(
if (envOnlyRouteId) return envOnlyRouteId

if (isEnvTruthy(processEnv.CLAUDE_CODE_USE_OPENAI)) {
const baseUrl =
processEnv.OPENAI_BASE_URL ?? processEnv.OPENAI_API_BASE
const baseUrl = hasNonEmptyEnvValue(processEnv.OPENAI_BASE_URL)
? processEnv.OPENAI_BASE_URL
: hasNonEmptyEnvValue(processEnv.OPENAI_API_BASE)
? processEnv.OPENAI_API_BASE
: undefined
const hasExplicitBaseUrl = baseUrl !== undefined
const matchedRoute = resolveRouteIdFromBaseUrl(baseUrl)

if (matchedRoute) {
return matchedRoute
}

if (options?.activeProfileProvider) {
if (!hasExplicitBaseUrl && options?.activeProfileProvider) {
const route = resolveProfileRoute(options.activeProfileProvider)
if (
route.routeId !== 'unknown-fallback' &&
Expand Down Expand Up @@ -1425,7 +1444,7 @@ export function resolveActiveRouteIdFromEnv(
return 'custom'
}

if (options?.activeProfileProvider) {
if (!anthropicBaseUrl && options?.activeProfileProvider) {
const route = resolveProfileRoute(options.activeProfileProvider)
if (
route.routeId !== 'unknown-fallback' &&
Expand Down
Loading
Loading