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:
Bogdan
2026-08-15 17:05:49 +08:00
committed by GitHub
parent 6c7a12b2a2
commit ea655163d3
17 changed files with 389 additions and 10 deletions
+5
View File
@@ -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
+1 -1
View File
@@ -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
+14 -1
View File
@@ -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:
+1
View File
@@ -13,6 +13,7 @@ export default defineBrand({
supportsPreciseTokenCount: false,
},
modelIds: [
'glm-5.3',
'glm-5.2',
'GLM-5.1',
'GLM-5-Turbo',
+5
View File
@@ -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
}
+4
View File
@@ -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',
+105 -1
View File
@@ -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({
+8 -2
View File
@@ -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,
+16
View File
@@ -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',
+95
View File
@@ -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
+6
View File
@@ -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,
+46
View File
@@ -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
View File
@@ -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'
+2
View File
@@ -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)
})
+36
View File
@@ -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)
+14 -2
View File
@@ -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: [