fix(cliproxy): harden xAI provider contracts

This commit is contained in:
Tam Nhu Tran committed 2026-07-18 19:04:07 -04:00
1 parent 90c0409aac
commit 4e4c949967
8 files changed
+110 -15

No files matched your search

@@ -63,6 +63,18 @@ describe('model-catalog compatibility lookups', () => {
});
});
it('does not export extended-context capability from live xAI context length', () => {
const catalog = mergeCatalog('xai', [
{
id: 'grok-4.3',
display_name: 'Grok 4.3',
context_length: 2_000_000,
},
]);
expect(catalog?.models[0]?.extendedContext).toBeUndefined();
});
it('does not re-add stale static-only models when live catalog data is present', () => {
const catalog = mergeCatalog('gemini', [
{
@@ -87,6 +87,23 @@ describe('Model Catalog', () => {
'grok-composer-2.5-fast',
]);
});
it('does not expose Claude [1m] suffix support for xAI model IDs', () => {
const { MODEL_CATALOG, supportsExtendedContext } = modelCatalog;
for (const model of MODEL_CATALOG.xai.models) {
assert.notStrictEqual(
model.extendedContext,
true,
`${model.id} must not advertise extended-context metadata`
);
assert.strictEqual(
supportsExtendedContext('xai', model.id),
false,
`${model.id} must not advertise [1m] suffix support`
);
}
});
});
describe('AGY models', () => {
@@ -7,6 +7,7 @@ import {
extractDeviceCodePrompt,
getExpectedLocalCallback,
getKiroBuilderIdSelectionInput,
resolveDeviceCodeVerificationUrl,
validateManualCallbackUrl,
} from '../oauth-process';
@@ -24,6 +25,18 @@ Then enter this code: ABCD-1234
verificationUrl: 'https://accounts.x.ai/oauth2/device?user_code=ABCD-1234',
});
});
it('preserves a parsed xAI verification URL exactly', () => {
const parsedUrl = 'https://accounts.x.ai/oauth2/device?user_code=ABCD-1234&source=cliproxy';
expect(resolveDeviceCodeVerificationUrl('xai', 'ABCD-1234', parsedUrl)).toBe(parsedUrl);
});
it('uses an xAI-specific device URL when upstream output omits the URL', () => {
expect(resolveDeviceCodeVerificationUrl('xai', 'ABCD-1234', null)).toBe(
'https://accounts.x.ai/oauth2/device?user_code=ABCD-1234'
);
});
});
describe('oauth-process stderr parsing', () => {
@@ -49,6 +62,13 @@ describe('oauth-process stderr parsing', () => {
expect(extractLikelyAuthFailureFromStderr('ghcp', stderr)).toBe('state mismatch');
});
it('extracts the current upstream xAI authentication failure format', () => {
const stderr =
'time="2026-07-18T00:00:00Z" level=error msg="xAI authentication failed: xai device code expired"';
expect(extractLikelyAuthFailureFromStderr('xai', stderr)).toBe('xai device code expired');
});
it('caps extracted message length to prevent noisy broadcasts', () => {
const longSuffix = 'x'.repeat(400);
const stderr = `level=error msg="Authentication failed: ${longSuffix}"`;
+20 -1
View File
@@ -143,6 +143,20 @@ export function extractDeviceCodePrompt(output: string): {
};
}
export function resolveDeviceCodeVerificationUrl(
provider: CLIProxyProvider,
userCode: string,
parsedUrl: string | null
): string {
if (parsedUrl) {
return parsedUrl;
}
if (provider === 'xai') {
return `https://accounts.x.ai/oauth2/device?user_code=${encodeURIComponent(userCode)}`;
}
return 'https://github.com/login/device';
}
export function isLoopbackHost(hostname: string): boolean {
const normalized = hostname.replace(/^\[|\]$/g, '').toLowerCase();
return (
@@ -415,7 +429,11 @@ async function handleStdout(
state.deviceCodeDisplayed = true;
log(`Parsed device code: ${state.userCode}`);
const verificationUrl = devicePrompt.verificationUrl || 'https://github.com/login/device';
const verificationUrl = resolveDeviceCodeVerificationUrl(
options.provider,
state.userCode,
devicePrompt.verificationUrl
);
// Emit device code event for WebSocket broadcast to UI
const deviceCodePrompt: DeviceCodePrompt = {
@@ -525,6 +543,7 @@ export function extractLikelyAuthFailureFromLogs(
});
const providerPatterns: Partial<Record<CLIProxyProvider, RegExp[]>> = {
xai: [/xai authentication failed:\s*(.+)/i],
ghcp: [
/github copilot authentication failed:\s*(.+)/i,
/failed to verify copilot access[^:]*:\s*(.+)/i,
+1 -4
View File
@@ -325,7 +325,6 @@ export const MODEL_CATALOG: Partial<Record<CLIProxyProvider, ProviderCatalog>> =
id: 'grok-4.3',
name: 'Grok 4.3',
description: 'General-purpose Grok model with a one-million-token context window',
extendedContext: true,
thinking: {
type: 'levels',
levels: ['none', 'low', 'medium', 'high'],
@@ -336,19 +335,16 @@ export const MODEL_CATALOG: Partial<Record<CLIProxyProvider, ProviderCatalog>> =
id: 'grok-4.20-0309-reasoning',
name: 'Grok 4.20 0309 Reasoning',
description: 'Reasoning model with a two-million-token context window',
extendedContext: true,
},
{
id: 'grok-4.20-0309-non-reasoning',
name: 'Grok 4.20 0309 Non Reasoning',
description: 'Non-reasoning model with a two-million-token context window',
extendedContext: true,
},
{
id: 'grok-4.20-multi-agent-0309',
name: 'Grok 4.20 Multi Agent 0309',
description: 'Multi-agent model with a two-million-token context window',
extendedContext: true,
thinking: { type: 'levels', levels: ['low', 'medium', 'high'] },
},
{
@@ -765,6 +761,7 @@ export function supportsThinking(provider: CLIProxyProvider, modelId: string): b
* Returns true if model has extendedContext: true in catalog.
*/
export function supportsExtendedContext(provider: CLIProxyProvider, modelId: string): boolean {
if (provider === 'xai') return false;
const model = findModel(provider, modelId);
return model?.extendedContext === true;
}
+5 -3
View File
@@ -215,13 +215,15 @@ function mapThinking(remote?: RemoteThinkingSupport): ThinkingSupport | undefine
}
/** Map RemoteModelInfo to ModelEntry */
function mapRemoteToModelEntry(remote: RemoteModelInfo): ModelEntry {
function mapRemoteToModelEntry(provider: CLIProxyProvider, remote: RemoteModelInfo): ModelEntry {
const entry: ModelEntry = {
id: remote.id,
name: remote.display_name || remote.id,
};
if (remote.description) entry.description = remote.description;
if (remote.context_length && remote.context_length >= 1_000_000) {
// xAI context length is inherent to the model ID; its API does not accept
// Claude's [1m] model suffix.
if (provider !== 'xai' && remote.context_length && remote.context_length >= 1_000_000) {
entry.extendedContext = true;
}
const thinking = mapThinking(remote.thinking);
@@ -264,7 +266,7 @@ export function mergeCatalog(
const mergedModels: ModelEntry[] = [];
for (const remote of filteredRemoteModels) {
const remoteEntry = mapRemoteToModelEntry(remote);
const remoteEntry = mapRemoteToModelEntry(provider, remote);
const staticEntry = staticMap.get(remote.id.toLowerCase());
if (staticEntry) {
const mergedThinking = remoteEntry.thinking
+7 -6
View File
@@ -407,25 +407,21 @@ export const MODEL_CATALOGS: Record<string, ProviderCatalog> = {
id: 'grok-4.3',
name: 'Grok 4.3',
description: 'General-purpose Grok model with a one-million-token context window',
extendedContext: true,
},
{
id: 'grok-4.20-0309-reasoning',
name: 'Grok 4.20 0309 Reasoning',
description: 'Reasoning model with a two-million-token context window',
extendedContext: true,
},
{
id: 'grok-4.20-0309-non-reasoning',
name: 'Grok 4.20 0309 Non Reasoning',
description: 'Non-reasoning model with a two-million-token context window',
extendedContext: true,
},
{
id: 'grok-4.20-multi-agent-0309',
name: 'Grok 4.20 Multi Agent 0309',
description: 'Multi-agent model with a two-million-token context window',
extendedContext: true,
},
{
id: 'grok-3-mini',
@@ -1124,7 +1120,8 @@ export function buildUiCatalog(
provider: string,
liveCatalog: ProviderCatalog | undefined
): ProviderCatalog | undefined {
const staticCatalog = MODEL_CATALOGS[provider.toLowerCase()];
const normalizedProvider = provider.toLowerCase();
const staticCatalog = MODEL_CATALOGS[normalizedProvider];
if (!liveCatalog || liveCatalog.models.length === 0) {
return staticCatalog;
}
@@ -1145,7 +1142,10 @@ export function buildUiCatalog(
issueUrl: staticModel?.issueUrl,
deprecated: staticModel?.deprecated,
deprecationReason: staticModel?.deprecationReason,
extendedContext: model.extendedContext ?? staticModel?.extendedContext,
extendedContext:
normalizedProvider === 'xai'
? undefined
: (model.extendedContext ?? staticModel?.extendedContext),
presetMapping: staticModel?.presetMapping,
};
});
@@ -1344,5 +1344,6 @@ export function supportsExtendedContext(
modelId: string,
catalogOverride?: ProviderCatalog
): boolean {
if (provider.toLowerCase() === 'xai') return false;
return findCatalogModel(provider, modelId, catalogOverride)?.extendedContext === true;
}
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest';
import { MODEL_CATALOGS } from '@/lib/model-catalogs';
import { buildUiCatalog, MODEL_CATALOGS, supportsExtendedContext } from '@/lib/model-catalogs';
describe('xAI model catalog defaults', () => {
it('mirrors the CLIProxyAPI text catalog and default tier routing', () => {
@@ -27,4 +27,31 @@ describe('xAI model catalog defaults', () => {
haiku: 'grok-composer-2.5-fast',
});
});
it('does not expose Claude [1m] suffix support for xAI model IDs', () => {
const catalog = MODEL_CATALOGS.xai;
for (const model of catalog.models) {
expect(model.extendedContext).not.toBe(true);
expect(supportsExtendedContext('xai', model.id)).toBe(false);
}
});
it('strips generic extended-context metadata from live xAI catalogs', () => {
const catalog = buildUiCatalog('xai', {
provider: 'xai',
displayName: 'xAI (Grok)',
defaultModel: 'grok-4.3',
models: [
{
id: 'grok-4.3',
name: 'Grok 4.3',
extendedContext: true,
},
],
});
expect(catalog?.models[0]?.extendedContext).toBeUndefined();
expect(supportsExtendedContext('xai', 'grok-4.3', catalog)).toBe(false);
});
});