Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
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
131 changes: 121 additions & 10 deletions src/integrations/runtimeMetadata.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
getDiscoveryCacheKey,
getRouteDiscoveryHeaders,
} from './discoveryService'
import { resolveActiveRouteIdFromEnv } from './routeMetadata.js'
import { setClaudeConfigHomeDirForTesting } from '../utils/envUtils.js'
import glmBrand from './brands/glm.js'
import glmModels from './models/glm.js'
Expand All @@ -24,6 +25,53 @@ import zaiVendor from './vendors/zai.js'
const originalConfigDir = process.env.CLAUDE_CONFIG_DIR

describe('Z.AI GLM-5.3 descriptor contract', () => {
it('wires the verified GLM-5.3-Flash descriptor and direct catalog contract', () => {
const model = glmModels.find(candidate => candidate.id === 'glm-5.3-flash')
expect(model).toMatchObject({
id: 'glm-5.3-flash',
label: 'GLM 5.3 Flash',
vendorId: 'zai',
brandId: 'glm',
classification: ['chat', 'reasoning', 'vision', 'coding'],
defaultModel: 'glm-5.3-flash',
contextWindow: 1_000_000,
maxOutputTokens: 131_072,
runtimeMetadataScope: 'catalog',
capabilities: {
supportsVision: true,
supportsStreaming: true,
supportsFunctionCalling: true,
supportsJsonMode: true,
supportsReasoning: true,
supportsPreciseTokenCount: false,
},
})

expect(glmBrand.modelIds?.[0]).toBe('glm-5.3-flash')
expect(glmBrand.modelIds?.[1]).toBe('glm-5.3')

const catalogEntries = zaiVendor.catalog?.models ?? []
const flashEntries = catalogEntries.filter(entry => entry.id === 'glm-5.3-flash')
expect(flashEntries).toHaveLength(1)
expect(catalogEntries[0]).toMatchObject({
id: 'glm-5.3-flash',
apiName: 'glm-5.3-flash',
label: 'GLM-5.3-Flash',
modelDescriptorId: 'glm-5.3-flash',
reasoning: {
mode: 'levels',
levels: ['low', 'high', 'xhigh'],
wireFormat: 'zai_compatible',
},
transportOverrides: {
openaiShim: { enableToolStreaming: true },
},
})
expect(catalogEntries[1]?.id).toBe('glm-5.3')
expect(zaiVendor.catalog?.source).toBe('static')
expect(zaiVendor.defaultModel).toBe('glm-5.2')
})

it('wires the verified shared model, brand, and direct catalog entry without changing the default', () => {
const model = glmModels.find(candidate => candidate.id === 'glm-5.3')
expect(model).toMatchObject({
Expand All @@ -45,9 +93,9 @@ describe('Z.AI GLM-5.3 descriptor contract', () => {
supportsPreciseTokenCount: false,
},
})
expect(glmBrand.modelIds?.[0]).toBe('glm-5.3')
expect(glmBrand.modelIds).toContain('glm-5.3')

const catalogEntry = zaiVendor.catalog?.models?.[0]
const catalogEntry = zaiVendor.catalog?.models?.find(entry => entry.id === 'glm-5.3')
expect(catalogEntry).toMatchObject({
id: 'glm-5.3',
apiName: 'glm-5.3',
Expand Down Expand Up @@ -166,6 +214,25 @@ describe('resolveModelRuntimeLimits', () => {
}
})
})
it.each([
'glm-5.3-flash',
'glm-5.3-flash?reasoning=low',
'glm-5.3-flash?reasoning=high',
'glm-5.3-flash?reasoning=xhigh',
'glm-5.3-flash?thinking=disabled',
])('uses verified Z.AI GLM-5.3-Flash runtime limits for %s', model => {
const limits = resolveModelRuntimeLimits({
model,
processEnv: {
CLAUDE_CODE_USE_OPENAI: '1',
OPENAI_BASE_URL: 'https://api.z.ai/api/coding/paas/v4',
},
})

expect(limits.contextWindow).toBe(1_000_000)
expect(limits.maxOutputTokens).toBe(131_072)
})

it.each([
'glm-5.3',
'glm-5.3?reasoning=low',
Expand Down Expand Up @@ -197,16 +264,37 @@ describe('resolveModelRuntimeLimits', () => {
})

it.each([
['NVIDIA NIM', 'https://integrate.api.nvidia.com/v1', { NVIDIA_NIM: '1' }],
['OpenRouter', 'https://openrouter.ai/api/v1', { CLAUDE_CODE_USE_OPENAI: '1' }],
['custom endpoint', 'https://proxy.example.test/v1', { CLAUDE_CODE_USE_OPENAI: '1' }],
] as const)('does not leak direct Z.AI GLM-5.3 limits onto %s', (_name, baseUrl, routeEnv) => {
['NVIDIA NIM', 'https://integrate.api.nvidia.com/v1', { CLAUDE_CODE_USE_OPENAI: '1', NVIDIA_NIM: '1' }, 'nvidia-nim'],
['OpenRouter', 'https://openrouter.ai/api/v1', { CLAUDE_CODE_USE_OPENAI: '1' }, 'openrouter'],
['custom endpoint', 'https://proxy.example.test/v1', { CLAUDE_CODE_USE_OPENAI: '1' }, 'custom'],
] as const)('does not leak direct Z.AI GLM-5.3-Flash limits onto %s', (_name, baseUrl, routeEnv, expectedRoute) => {
const processEnv = {
...routeEnv,
OPENAI_BASE_URL: baseUrl,
}
expect(resolveActiveRouteIdFromEnv(processEnv)).toBe(expectedRoute)
expect(resolveModelRuntimeLimits({
model: 'glm-5.3-flash',
processEnv,
})).toEqual({
contextWindow: undefined,
maxOutputTokens: undefined,
})
})

it.each([
['NVIDIA NIM', 'https://integrate.api.nvidia.com/v1', { CLAUDE_CODE_USE_OPENAI: '1', NVIDIA_NIM: '1' }, 'nvidia-nim'],
['OpenRouter', 'https://openrouter.ai/api/v1', { CLAUDE_CODE_USE_OPENAI: '1' }, 'openrouter'],
['custom endpoint', 'https://proxy.example.test/v1', { CLAUDE_CODE_USE_OPENAI: '1' }, 'custom'],
] as const)('does not leak direct Z.AI GLM-5.3 limits onto %s', (_name, baseUrl, routeEnv, expectedRoute) => {
const processEnv = {
...routeEnv,
OPENAI_BASE_URL: baseUrl,
}
expect(resolveActiveRouteIdFromEnv(processEnv)).toBe(expectedRoute)
expect(resolveModelRuntimeLimits({
model: 'glm-5.3',
processEnv: {
...routeEnv,
OPENAI_BASE_URL: baseUrl,
},
processEnv,
})).toEqual({
contextWindow: undefined,
maxOutputTokens: undefined,
Expand Down Expand Up @@ -431,6 +519,29 @@ describe('resolveOpenAIShimRuntimeContext - Z.A.I GLM-5.3', () => {
})
})

describe('resolveOpenAIShimRuntimeContext - Z.A.I GLM-5.3-Flash', () => {
it.each([
'glm-5.3-flash',
'glm-5.3-flash?reasoning=xhigh',
'glm-5.3-flash?thinking=disabled',
])('uses the explicit direct-route GLM-5.3-Flash contract for %s', model => {
const result = resolveOpenAIShimRuntimeContext({
model,
baseUrl: 'https://api.z.ai/api/coding/paas/v4',
processEnv: {},
})

expect(result.routeId).toBe('zai')
expect(result.catalogEntry?.id).toBe('glm-5.3-flash')
expect(result.openaiShimConfig.thinkingRequestFormat).toBe('zai-compatible')
expect(result.openaiShimConfig.preserveReasoningContent).toBe(true)
expect(result.openaiShimConfig.requireReasoningContentOnAssistantMessages).toBe(true)
expect(result.openaiShimConfig.maxTokensField).toBe('max_tokens')
expect(result.openaiShimConfig.removeBodyFields).toContain('store')
expect(result.openaiShimConfig.enableToolStreaming).toBe(true)
})
})

describe('resolveOpenAIShimRuntimeContext - GLM on a non-Z.AI gateway (#1896)', () => {
it('infers the GLM reasoning shim but not tool streaming for a third-party gateway', () => {
const result = resolveOpenAIShimRuntimeContext({
Expand Down
16 changes: 16 additions & 0 deletions src/integrations/vendors/zai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,22 @@ export default defineVendor({
catalog: {
source: 'static',
models: [
{
id: 'glm-5.3-flash',
apiName: 'glm-5.3-flash',
label: 'GLM-5.3-Flash',
modelDescriptorId: 'glm-5.3-flash',
reasoning: {
mode: 'levels',
levels: ['low', 'high', 'xhigh'],
wireFormat: 'zai_compatible',
},
transportOverrides: {
openaiShim: {
enableToolStreaming: true,
},
},
},
{
id: 'glm-5.3',
apiName: 'glm-5.3',
Expand Down
Loading
Loading