mirror of
https://github.com/Gitlawb/openclaude.git
synced 2026-08-24 10:14:19 -05:00
feat(zai): expand Coding Plan catalog support (#2127)
* feat(zai): expand Coding Plan catalog support Signed-off-by: chioarub <chioarub@gmail.com> * fix(zai): use supported low reasoning mode Signed-off-by: chioarub <chioarub@gmail.com> --------- Signed-off-by: chioarub <chioarub@gmail.com>
This commit is contained in:
@@ -192,7 +192,12 @@ 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.1, GLM-5-Turbo, GLM-4.7, or GLM-4.5-Air
|
||||
# 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
|
||||
# OPENAI_MODEL='glm-5.3?reasoning=xhigh' # maps to Z.AI reasoning_effort=max
|
||||
# Optional GLM-5.2 thinking controls:
|
||||
# OPENAI_MODEL='glm-5.2?reasoning=high' # enhanced reasoning
|
||||
# OPENAI_MODEL='glm-5.2?reasoning=xhigh' # maps to Z.AI reasoning_effort=max
|
||||
|
||||
@@ -343,7 +343,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. Use `glm-5.2?reasoning=high` for enhanced reasoning, `glm-5.2?reasoning=xhigh` to request Z.AI `reasoning_effort=max`, or `glm-5.2?thinking=disabled` for faster direct answers.
|
||||
- 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.
|
||||
- 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
|
||||
|
||||
|
||||
@@ -31,7 +31,10 @@ still the source of truth for where a model is offered.
|
||||
capabilities.
|
||||
4. Add optional shared metadata.
|
||||
Include `brandId`, `contextWindow`, `maxOutputTokens`, and `cacheConfig`
|
||||
when the data is stable enough to be reused.
|
||||
when the data is stable enough to be reused. Set
|
||||
`runtimeMetadataScope: 'catalog'` when verified limits and capabilities
|
||||
should apply only on route catalogs that explicitly reference the
|
||||
descriptor.
|
||||
5. Add `providerModelMap` only when the same model needs route-specific API
|
||||
names across multiple catalogs.
|
||||
6. Update route-owned catalogs only if the model should be offered by those
|
||||
@@ -47,6 +50,11 @@ Model descriptor files should:
|
||||
- avoid encoding gateway availability as if every route automatically exposes
|
||||
the shared model.
|
||||
|
||||
Shared runtime metadata uses the legacy global model-name fallback by default.
|
||||
Use `runtimeMetadataScope: 'catalog'` for a model whose verified limits and
|
||||
capabilities belong to specific routes; that metadata then applies only when a
|
||||
route catalog entry names the descriptor through `modelDescriptorId`.
|
||||
|
||||
Normal contributor-facing examples should not call `registerModel(...)`
|
||||
directly.
|
||||
|
||||
@@ -190,6 +198,11 @@ Model lookup should prefer:
|
||||
second built-in model table. Built-in model limits belong in model descriptor
|
||||
files.
|
||||
|
||||
A descriptor with `runtimeMetadataScope: 'catalog'` is intentionally excluded
|
||||
from global name-only lookups. Its limits and capabilities are available only
|
||||
through an explicit route catalog entry, preventing one vendor's verified
|
||||
contract from leaking onto an uncataloged gateway model with the same API name.
|
||||
|
||||
## What not to do
|
||||
|
||||
Avoid these patterns:
|
||||
|
||||
@@ -13,6 +13,7 @@ export default defineBrand({
|
||||
supportsPreciseTokenCount: false,
|
||||
},
|
||||
modelIds: [
|
||||
'glm-5.3',
|
||||
'glm-5.2',
|
||||
'GLM-5.1',
|
||||
'GLM-5-Turbo',
|
||||
|
||||
@@ -355,6 +355,11 @@ export interface ModelDescriptor {
|
||||
reasoning?: ReasoningControlMetadata
|
||||
contextWindow?: number
|
||||
maxOutputTokens?: number
|
||||
/**
|
||||
* Restrict shared runtime metadata to catalog entries that explicitly
|
||||
* reference this descriptor. Omit for the legacy global model-name fallback.
|
||||
*/
|
||||
runtimeMetadataScope?: 'global' | 'catalog'
|
||||
cacheConfig?: CacheConfig
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { defineModel } from '../define.js'
|
||||
import type { ModelDescriptor } from '../descriptors.js'
|
||||
|
||||
const glmCapabilities = {
|
||||
supportsVision: false,
|
||||
@@ -14,6 +15,7 @@ function glmModel(
|
||||
label: string,
|
||||
contextWindow: number,
|
||||
maxOutputTokens: number,
|
||||
runtimeMetadataScope?: ModelDescriptor['runtimeMetadataScope'],
|
||||
) {
|
||||
return defineModel({
|
||||
id,
|
||||
@@ -25,10 +27,12 @@ function glmModel(
|
||||
capabilities: glmCapabilities,
|
||||
contextWindow,
|
||||
maxOutputTokens,
|
||||
...(runtimeMetadataScope ? { runtimeMetadataScope } : {}),
|
||||
})
|
||||
}
|
||||
|
||||
export default [
|
||||
glmModel('glm-5.3', 'GLM 5.3', 1_000_000, 131_072, 'catalog'),
|
||||
defineModel({
|
||||
id: 'glm-5v-turbo',
|
||||
label: 'GLM 5V Turbo',
|
||||
|
||||
@@ -17,9 +17,55 @@ import {
|
||||
getRouteDiscoveryHeaders,
|
||||
} from './discoveryService'
|
||||
import { setClaudeConfigHomeDirForTesting } from '../utils/envUtils.js'
|
||||
import glmBrand from './brands/glm.js'
|
||||
import glmModels from './models/glm.js'
|
||||
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 shared model, brand, and direct catalog entry without changing the default', () => {
|
||||
const model = glmModels.find(candidate => candidate.id === 'glm-5.3')
|
||||
expect(model).toMatchObject({
|
||||
id: 'glm-5.3',
|
||||
label: 'GLM 5.3',
|
||||
vendorId: 'zai',
|
||||
brandId: 'glm',
|
||||
classification: ['chat', 'reasoning', 'coding'],
|
||||
defaultModel: 'glm-5.3',
|
||||
contextWindow: 1_000_000,
|
||||
maxOutputTokens: 131_072,
|
||||
runtimeMetadataScope: 'catalog',
|
||||
capabilities: {
|
||||
supportsVision: false,
|
||||
supportsStreaming: true,
|
||||
supportsFunctionCalling: true,
|
||||
supportsJsonMode: true,
|
||||
supportsReasoning: true,
|
||||
supportsPreciseTokenCount: false,
|
||||
},
|
||||
})
|
||||
expect(glmBrand.modelIds?.[0]).toBe('glm-5.3')
|
||||
|
||||
const catalogEntry = zaiVendor.catalog?.models?.[0]
|
||||
expect(catalogEntry).toMatchObject({
|
||||
id: 'glm-5.3',
|
||||
apiName: 'glm-5.3',
|
||||
label: 'GLM-5.3',
|
||||
modelDescriptorId: 'glm-5.3',
|
||||
reasoning: {
|
||||
mode: 'levels',
|
||||
levels: ['low', 'high', 'xhigh'],
|
||||
wireFormat: 'zai_compatible',
|
||||
},
|
||||
transportOverrides: {
|
||||
openaiShim: { enableToolStreaming: true },
|
||||
},
|
||||
})
|
||||
expect(zaiVendor.defaultModel).toBe('glm-5.2')
|
||||
})
|
||||
})
|
||||
|
||||
async function withTempConfigDir<T>(fn: () => Promise<T>): Promise<T> {
|
||||
await acquireSharedMutationLock('integrations/runtimeMetadata.test.ts')
|
||||
let tempDir: string | null = null
|
||||
@@ -120,7 +166,25 @@ describe('resolveModelRuntimeLimits', () => {
|
||||
}
|
||||
})
|
||||
})
|
||||
it('uses built-in Z.AI GLM-5.2 runtime limits', () => {
|
||||
it.each([
|
||||
'glm-5.3',
|
||||
'glm-5.3?reasoning=low',
|
||||
'glm-5.3?reasoning=xhigh',
|
||||
'glm-5.3?thinking=disabled',
|
||||
])('uses verified Z.AI GLM-5.3 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('keeps the built-in Z.AI GLM-5.2 runtime limits', () => {
|
||||
const limits = resolveModelRuntimeLimits({
|
||||
model: 'glm-5.2',
|
||||
processEnv: {
|
||||
@@ -131,6 +195,23 @@ describe('resolveModelRuntimeLimits', () => {
|
||||
expect(limits.contextWindow).toBe(1_000_000)
|
||||
expect(limits.maxOutputTokens).toBe(131_072)
|
||||
})
|
||||
|
||||
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) => {
|
||||
expect(resolveModelRuntimeLimits({
|
||||
model: 'glm-5.3',
|
||||
processEnv: {
|
||||
...routeEnv,
|
||||
OPENAI_BASE_URL: baseUrl,
|
||||
},
|
||||
})).toEqual({
|
||||
contextWindow: undefined,
|
||||
maxOutputTokens: undefined,
|
||||
})
|
||||
})
|
||||
it('uses the applied provider profile route before generic custom base URL fallback', () => {
|
||||
expect(
|
||||
resolveModelRuntimeLimits({
|
||||
@@ -309,6 +390,29 @@ describe('resolveOpenAIShimRuntimeContext - Z.A.I GLM-5.2', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('resolveOpenAIShimRuntimeContext - Z.A.I GLM-5.3', () => {
|
||||
it.each([
|
||||
'glm-5.3',
|
||||
'glm-5.3?reasoning=xhigh',
|
||||
'glm-5.3?thinking=disabled',
|
||||
])('uses the explicit direct-route GLM-5.3 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')
|
||||
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({
|
||||
|
||||
@@ -509,10 +509,16 @@ export function resolveModelRuntimeLimits(options: {
|
||||
modelApiName,
|
||||
runtimeEnv,
|
||||
)
|
||||
const modelDescriptor =
|
||||
const catalogModelDescriptor =
|
||||
getModelDescriptorForCatalogEntry(catalogEntry) ??
|
||||
getModelDescriptorForCatalogEntry(cachedCatalogEntry) ??
|
||||
getModelDescriptorForCatalogEntry(cachedCatalogEntry)
|
||||
const inferredModelDescriptor =
|
||||
findModelDescriptorForApiName(routeId, modelApiName)
|
||||
const modelDescriptor =
|
||||
catalogModelDescriptor ??
|
||||
(inferredModelDescriptor?.runtimeMetadataScope === 'catalog'
|
||||
? null
|
||||
: inferredModelDescriptor)
|
||||
const externalContextWindow = getOpenAIContextWindowMatches(
|
||||
modelApiName,
|
||||
runtimeEnv,
|
||||
|
||||
Vendored
+16
@@ -44,6 +44,22 @@ export default defineVendor({
|
||||
catalog: {
|
||||
source: 'static',
|
||||
models: [
|
||||
{
|
||||
id: 'glm-5.3',
|
||||
apiName: 'glm-5.3',
|
||||
label: 'GLM-5.3',
|
||||
modelDescriptorId: 'glm-5.3',
|
||||
reasoning: {
|
||||
mode: 'levels',
|
||||
levels: ['low', 'high', 'xhigh'],
|
||||
wireFormat: 'zai_compatible',
|
||||
},
|
||||
transportOverrides: {
|
||||
openaiShim: {
|
||||
enableToolStreaming: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'glm-5.2',
|
||||
apiName: 'glm-5.2',
|
||||
|
||||
@@ -5437,6 +5437,100 @@ test.each([
|
||||
expect(requestBody?.reasoning_effort).toBe(effort)
|
||||
})
|
||||
|
||||
test.each([
|
||||
['glm-5.3', undefined, undefined],
|
||||
['glm-5.3?reasoning=low', 'enabled', 'low'],
|
||||
['glm-5.3?reasoning=high', 'enabled', 'high'],
|
||||
['glm-5.3?reasoning=xhigh', 'enabled', 'max'],
|
||||
['glm-5.3?thinking=disabled', 'enabled', 'low'],
|
||||
['glm-5.3?thinking=disabled&reasoning=high', 'enabled', 'high'],
|
||||
] as const)('Z.AI GLM-5.3 serializes the verified request contract for %s', async (
|
||||
model,
|
||||
thinkingType,
|
||||
reasoningEffort,
|
||||
) => {
|
||||
process.env.OPENAI_BASE_URL = 'https://api.z.ai/api/coding/paas/v4'
|
||||
process.env.OPENAI_API_KEY = 'sk-zai-test'
|
||||
|
||||
let requestBody: Record<string, unknown> | undefined
|
||||
globalThis.fetch = (async (_input, init) => {
|
||||
requestBody = JSON.parse(String(init?.body))
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
id: 'chatcmpl-1',
|
||||
model: 'glm-5.3',
|
||||
choices: [
|
||||
{ message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' },
|
||||
],
|
||||
}),
|
||||
{ headers: { 'Content-Type': 'application/json' } },
|
||||
)
|
||||
}) as unknown as FetchType
|
||||
|
||||
const client = createOpenAIShimClient({}) as OpenAIShimClient
|
||||
await client.beta.messages.create({
|
||||
model,
|
||||
messages: [{ role: 'user', content: 'hi' }],
|
||||
max_tokens: 64,
|
||||
stream: false,
|
||||
})
|
||||
|
||||
expect(requestBody?.model).toBe('glm-5.3')
|
||||
expect(requestBody?.max_tokens).toBe(64)
|
||||
expect(requestBody?.max_completion_tokens).toBeUndefined()
|
||||
expect(requestBody?.store).toBeUndefined()
|
||||
expect(requestBody?.thinking).toEqual(
|
||||
thinkingType ? { type: thinkingType } : undefined,
|
||||
)
|
||||
expect(requestBody?.reasoning_effort).toBe(reasoningEffort)
|
||||
})
|
||||
|
||||
test('streaming direct Z.AI GLM-5.3 tool requests opt into tool_stream', async () => {
|
||||
process.env.OPENAI_BASE_URL = 'https://api.z.ai/api/coding/paas/v4'
|
||||
process.env.OPENAI_API_KEY = 'sk-zai-test'
|
||||
|
||||
let requestBody: Record<string, unknown> | undefined
|
||||
globalThis.fetch = (async (_input, init) => {
|
||||
requestBody = JSON.parse(String(init?.body))
|
||||
return makeSseResponse(makeStreamChunks([
|
||||
{
|
||||
id: 'chatcmpl-1',
|
||||
object: 'chat.completion.chunk',
|
||||
model: 'glm-5.3',
|
||||
choices: [{ index: 0, delta: { content: 'ok' }, finish_reason: null }],
|
||||
},
|
||||
{
|
||||
id: 'chatcmpl-1',
|
||||
object: 'chat.completion.chunk',
|
||||
model: 'glm-5.3',
|
||||
choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],
|
||||
},
|
||||
]))
|
||||
}) as unknown as FetchType
|
||||
|
||||
const client = createOpenAIShimClient({}) as OpenAIShimClient
|
||||
const stream = await client.beta.messages.create({
|
||||
model: 'glm-5.3',
|
||||
messages: [{ role: 'user', content: 'add two numbers' }],
|
||||
max_tokens: 64,
|
||||
stream: true,
|
||||
tools: [{
|
||||
name: 'add_numbers',
|
||||
description: 'Add two numbers',
|
||||
input_schema: {
|
||||
type: 'object',
|
||||
properties: { a: { type: 'number' }, b: { type: 'number' } },
|
||||
required: ['a', 'b'],
|
||||
},
|
||||
}],
|
||||
})
|
||||
for await (const _event of stream as AsyncIterable<unknown>) {
|
||||
// Drain the mocked response so request execution completes.
|
||||
}
|
||||
|
||||
expect(requestBody?.tool_stream).toBe(true)
|
||||
})
|
||||
|
||||
test.each([
|
||||
'GLM-5.1?reasoning=high',
|
||||
'GLM-4.5-Air?reasoning=high',
|
||||
@@ -5477,6 +5571,7 @@ test.each([
|
||||
test.each([
|
||||
['non-streaming Z.AI request with tools', 'https://api.z.ai/api/coding/paas/v4', false, true, 'glm-5.2'],
|
||||
['streaming Z.AI request without tools', 'https://api.z.ai/api/coding/paas/v4', true, false, 'glm-5.2'],
|
||||
['streaming NVIDIA GLM-5.3 request with tools', 'https://integrate.api.nvidia.com/v1', true, true, 'glm-5.3'],
|
||||
['streaming non-Z.AI request with tools', 'https://api.openai.com/v1', true, true, 'gpt-4o'],
|
||||
] as const)('does not send tool_stream for %s', async (_name, baseUrl, stream, includeTools, model) => {
|
||||
process.env.OPENAI_BASE_URL = baseUrl
|
||||
|
||||
@@ -907,8 +907,14 @@ test('DashScope glm-4.7 uses provider-specific context and output caps', () => {
|
||||
|
||||
test('Z.AI GLM models use Coding Plan output caps', () => {
|
||||
process.env.CLAUDE_CODE_USE_OPENAI = '1'
|
||||
process.env.OPENAI_BASE_URL = 'https://api.z.ai/api/coding/paas/v4'
|
||||
delete process.env.CLAUDE_CODE_MAX_OUTPUT_TOKENS
|
||||
|
||||
expect(getContextWindowForModel('glm-5.3')).toBe(1_000_000)
|
||||
expect(getModelMaxOutputTokens('glm-5.3')).toEqual({
|
||||
default: 131_072,
|
||||
upperLimit: 131_072,
|
||||
})
|
||||
expect(getContextWindowForModel('glm-5.2')).toBe(1_000_000)
|
||||
expect(getModelMaxOutputTokens('glm-5.2')).toEqual({
|
||||
default: 131_072,
|
||||
|
||||
@@ -1150,6 +1150,31 @@ test('compat Z.AI routes expose only verified levels and clamp stale values', as
|
||||
expect(resolveAppliedEffort('GLM-5.1', 'xhigh')).toBe('high')
|
||||
})
|
||||
|
||||
test('direct Z.AI GLM-5.3 resolves effort from explicit catalog metadata', async () => {
|
||||
const {
|
||||
getAvailableEffortLevels,
|
||||
resolveAppliedEffort,
|
||||
resolveModelReasoningControl,
|
||||
} = await importFreshEffortModule({
|
||||
provider: 'openai',
|
||||
supportsCodexReasoningEffort: false,
|
||||
routeId: 'zai',
|
||||
})
|
||||
|
||||
expect(resolveModelReasoningControl('glm-5.3')).toMatchObject({
|
||||
supportsReasoning: true,
|
||||
controllable: true,
|
||||
source: 'metadata',
|
||||
mode: 'levels',
|
||||
levels: ['low', 'high', 'xhigh'],
|
||||
defaultLevel: undefined,
|
||||
wireFormat: 'zai_compatible',
|
||||
})
|
||||
expect(getAvailableEffortLevels('glm-5.3')).toEqual(['low', 'high', 'xhigh'])
|
||||
expect(resolveAppliedEffort('glm-5.3', 'low')).toBe('low')
|
||||
expect(resolveAppliedEffort('glm-5.3', 'xhigh')).toBe('xhigh')
|
||||
})
|
||||
|
||||
test('provider override support context ignores ambient catalog metadata', async () => {
|
||||
const { modelSupportsShimReasoningEffort } = await importFreshEffortModule({
|
||||
provider: 'openai',
|
||||
@@ -1344,6 +1369,27 @@ test('explicit compat metadata wire formats are controllable and feed the reques
|
||||
model: 'custom-zai-low-only',
|
||||
requestedEffort: 'low',
|
||||
reasoningControl: zaiLowOnlyControl,
|
||||
})).toEqual({
|
||||
thinkingType: 'enabled',
|
||||
reasoningEffort: 'low',
|
||||
wireFormat: 'zai_compatible',
|
||||
source: 'metadata',
|
||||
})
|
||||
expect(resolveOpenAIShimReasoningRequestPlan({
|
||||
model: 'custom-zai-low-only',
|
||||
requestThinkingType: 'disabled',
|
||||
reasoningControl: zaiLowOnlyControl,
|
||||
})).toEqual({
|
||||
thinkingType: 'enabled',
|
||||
reasoningEffort: 'low',
|
||||
wireFormat: 'zai_compatible',
|
||||
source: 'metadata',
|
||||
})
|
||||
expect(resolveOpenAIShimReasoningRequestPlan({
|
||||
model: 'custom-zai-low-only',
|
||||
requestedEffort: 'high',
|
||||
requestThinkingType: 'disabled',
|
||||
reasoningControl: zaiLowOnlyControl,
|
||||
})).toEqual({
|
||||
thinkingType: 'enabled',
|
||||
reasoningEffort: 'high',
|
||||
|
||||
+16
-3
@@ -208,7 +208,9 @@ function normalizeDeepSeekReasoningEffort(
|
||||
|
||||
function normalizeZaiReasoningEffort(
|
||||
effort: OpenAIShimEffortLevel,
|
||||
): 'high' | 'max' {
|
||||
supportsLowEffort = false,
|
||||
): 'low' | 'high' | 'max' {
|
||||
if (supportsLowEffort && effort === 'low') return 'low'
|
||||
return effort === 'xhigh' || effort === 'max' ? 'max' : 'high'
|
||||
}
|
||||
|
||||
@@ -635,8 +637,16 @@ export function resolveOpenAIShimReasoningRequestPlan(options: {
|
||||
if (wireFormat === 'zai_compatible') {
|
||||
const thinkingType = requestedThinkingType ?? defaultThinkingType
|
||||
if (thinkingType === 'disabled') {
|
||||
const supportsLowEffort =
|
||||
metadataWireFormat === 'zai_compatible' &&
|
||||
options.reasoningControl?.levels.includes('low') === true &&
|
||||
options.reasoningControl.disableFormat !== 'thinking_type_disabled'
|
||||
const translatedEffort = supportsLowEffort && options.requestedEffort
|
||||
? normalizeZaiReasoningEffort(options.requestedEffort, true)
|
||||
: 'low'
|
||||
return {
|
||||
thinkingType: 'disabled',
|
||||
thinkingType: supportsLowEffort ? 'enabled' : 'disabled',
|
||||
reasoningEffort: supportsLowEffort ? translatedEffort : undefined,
|
||||
wireFormat,
|
||||
source,
|
||||
}
|
||||
@@ -651,7 +661,10 @@ export function resolveOpenAIShimReasoningRequestPlan(options: {
|
||||
metadataWireFormat !== 'zai_compatible' &&
|
||||
supportsZaiReasoningEffort(options.model)
|
||||
))
|
||||
? normalizeZaiReasoningEffort(options.requestedEffort)
|
||||
? normalizeZaiReasoningEffort(
|
||||
options.requestedEffort,
|
||||
options.reasoningControl?.levels.includes('low') === true,
|
||||
)
|
||||
: undefined
|
||||
return {
|
||||
thinkingType: shouldEnableThinking ? 'enabled' : undefined,
|
||||
|
||||
@@ -116,6 +116,21 @@ test('Kimi Code keeps context variants distinct in the active route picker', asy
|
||||
expect(options.find(option => option.value === 'k3-256k')?.label).toBe('Kimi K3 (256K)')
|
||||
})
|
||||
|
||||
test('Z.AI surfaces GLM-5.3 exactly once ahead of GLM-5.2 without changing the default', async () => {
|
||||
process.env.CLAUDE_CODE_USE_OPENAI = '1'
|
||||
process.env.OPENAI_BASE_URL = 'https://api.z.ai/api/coding/paas/v4'
|
||||
process.env.OPENAI_MODEL = 'glm-5.2'
|
||||
process.env.OPENAI_API_KEY = 'sk-zai-test'
|
||||
|
||||
const options = await getOpenAIModelOptions()
|
||||
const values = options.map(option => option.value)
|
||||
|
||||
expect(values.filter(value => value === 'glm-5.3')).toHaveLength(1)
|
||||
expect(values.indexOf('glm-5.3')).toBeLessThan(values.indexOf('glm-5.2'))
|
||||
expect(options.find(option => option.value === 'glm-5.3')?.label).toBe('GLM-5.3')
|
||||
expect(options.find(option => option.value === null)?.description).toContain('glm-5.2')
|
||||
})
|
||||
|
||||
test('custom Anthropic endpoints use the third-party default description', async () => {
|
||||
process.env.ANTHROPIC_BASE_URL = 'https://proxy.example/v1'
|
||||
process.env.ANTHROPIC_MODEL = 'proxy-model'
|
||||
|
||||
@@ -79,6 +79,8 @@ describe('modelSupportsThinking — Z.AI GLM', () => {
|
||||
expect(modelSupportsThinking('GLM-5-Turbo')).toBe(true)
|
||||
expect(modelSupportsThinking('GLM-4.7')).toBe(true)
|
||||
expect(modelSupportsThinking('GLM-4.5-Air')).toBe(true)
|
||||
expect(modelSupportsThinking('glm-5.3')).toBe(true)
|
||||
expect(modelSupportsThinking('glm-5.3?reasoning=low')).toBe(true)
|
||||
expect(modelSupportsThinking('glm-5.2?thinking=disabled')).toBe(true)
|
||||
expect(modelSupportsThinking('glm-5.2 ?thinking=disabled')).toBe(true)
|
||||
})
|
||||
|
||||
@@ -136,6 +136,24 @@ describe('isVisionSupported', () => {
|
||||
expect(isVisionSupported('gemini-2.5-pro')).toBe(true)
|
||||
})
|
||||
|
||||
test('scopes GLM-5.3 text-only metadata to the direct Z.AI catalog', () => {
|
||||
expect(
|
||||
isVisionSupported('glm-5.3', {
|
||||
baseUrl: 'https://api.z.ai/api/coding/paas/v4',
|
||||
}),
|
||||
).toBe(false)
|
||||
expect(
|
||||
isVisionSupported('glm-5.3', {
|
||||
baseUrl: 'https://integrate.api.nvidia.com/v1',
|
||||
}),
|
||||
).toBe(true)
|
||||
expect(
|
||||
isVisionSupported('glm-5.3', {
|
||||
baseUrl: 'https://proxy.example.test/v1',
|
||||
}),
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
test('falls open for unknown models so custom / non-registered providers keep working', () => {
|
||||
expect(isVisionSupported('custom-vision-corp/secret-model-v1')).toBe(true)
|
||||
expect(isVisionSupported('not-a-real-model-xyz')).toBe(true)
|
||||
@@ -196,6 +214,24 @@ describe('checkVisionCapabilityForFile (issue #1421)', () => {
|
||||
expect(result.result).toBe(true)
|
||||
})
|
||||
|
||||
test('only blocks GLM-5.3 image reads on the direct Z.AI route', () => {
|
||||
expect(
|
||||
checkVisionCapabilityForFile('x.png', 'glm-5.3', {
|
||||
baseUrl: 'https://api.z.ai/api/coding/paas/v4',
|
||||
}).result,
|
||||
).toBe(false)
|
||||
expect(
|
||||
checkVisionCapabilityForFile('x.png', 'glm-5.3', {
|
||||
baseUrl: 'https://integrate.api.nvidia.com/v1',
|
||||
}).result,
|
||||
).toBe(true)
|
||||
expect(
|
||||
checkVisionCapabilityForFile('x.png', 'glm-5.3', {
|
||||
baseUrl: 'https://proxy.example.test/v1',
|
||||
}).result,
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
test('does not gate text-file reads on non-vision models', () => {
|
||||
const result = checkVisionCapabilityForFile('C:\\foo\\bar.txt', 'mimo-v2.5-pro')
|
||||
expect(result.result).toBe(true)
|
||||
|
||||
@@ -37,6 +37,12 @@ function hasModelNamePrefix(modelApiName: string, registeredName: string): boole
|
||||
return next === undefined || next === ':' || next === '/' || next === '@'
|
||||
}
|
||||
|
||||
function isCatalogScopedRuntimeDescriptor(
|
||||
descriptor: ReturnType<typeof getModel> | undefined,
|
||||
): boolean {
|
||||
return descriptor?.runtimeMetadataScope === 'catalog'
|
||||
}
|
||||
|
||||
function findModelDescriptorFromCatalog(
|
||||
modelApiName: string,
|
||||
routeId?: string,
|
||||
@@ -100,15 +106,21 @@ export function findModelDescriptorForApiNameWithRoute(
|
||||
}
|
||||
|
||||
const direct = getModel(trimmed)
|
||||
if (direct) return direct
|
||||
if (direct && !isCatalogScopedRuntimeDescriptor(direct)) return direct
|
||||
|
||||
const catalogDescriptor = findModelDescriptorFromCatalog(trimmed)
|
||||
if (catalogDescriptor) return catalogDescriptor
|
||||
if (
|
||||
catalogDescriptor &&
|
||||
!isCatalogScopedRuntimeDescriptor(catalogDescriptor)
|
||||
) {
|
||||
return catalogDescriptor
|
||||
}
|
||||
|
||||
const normalized = normalizedName(trimmed)
|
||||
const models = getAllModels()
|
||||
|
||||
const candidates = models
|
||||
.filter(model => !isCatalogScopedRuntimeDescriptor(model))
|
||||
.map(model => ({
|
||||
model,
|
||||
names: [
|
||||
|
||||
Reference in New Issue
Block a user