mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-04 12:13:09 +00:00
fix(cliproxy): harden xAI provider contracts
This commit is contained in:
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}"`;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user