mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-11 12:09:03 +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', () => {
|
it('does not re-add stale static-only models when live catalog data is present', () => {
|
||||||
const catalog = mergeCatalog('gemini', [
|
const catalog = mergeCatalog('gemini', [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -87,6 +87,23 @@ describe('Model Catalog', () => {
|
|||||||
'grok-composer-2.5-fast',
|
'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', () => {
|
describe('AGY models', () => {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
extractDeviceCodePrompt,
|
extractDeviceCodePrompt,
|
||||||
getExpectedLocalCallback,
|
getExpectedLocalCallback,
|
||||||
getKiroBuilderIdSelectionInput,
|
getKiroBuilderIdSelectionInput,
|
||||||
|
resolveDeviceCodeVerificationUrl,
|
||||||
validateManualCallbackUrl,
|
validateManualCallbackUrl,
|
||||||
} from '../oauth-process';
|
} from '../oauth-process';
|
||||||
|
|
||||||
@@ -24,6 +25,18 @@ Then enter this code: ABCD-1234
|
|||||||
verificationUrl: 'https://accounts.x.ai/oauth2/device?user_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', () => {
|
describe('oauth-process stderr parsing', () => {
|
||||||
@@ -49,6 +62,13 @@ describe('oauth-process stderr parsing', () => {
|
|||||||
expect(extractLikelyAuthFailureFromStderr('ghcp', stderr)).toBe('state mismatch');
|
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', () => {
|
it('caps extracted message length to prevent noisy broadcasts', () => {
|
||||||
const longSuffix = 'x'.repeat(400);
|
const longSuffix = 'x'.repeat(400);
|
||||||
const stderr = `level=error msg="Authentication failed: ${longSuffix}"`;
|
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 {
|
export function isLoopbackHost(hostname: string): boolean {
|
||||||
const normalized = hostname.replace(/^\[|\]$/g, '').toLowerCase();
|
const normalized = hostname.replace(/^\[|\]$/g, '').toLowerCase();
|
||||||
return (
|
return (
|
||||||
@@ -415,7 +429,11 @@ async function handleStdout(
|
|||||||
state.deviceCodeDisplayed = true;
|
state.deviceCodeDisplayed = true;
|
||||||
log(`Parsed device code: ${state.userCode}`);
|
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
|
// Emit device code event for WebSocket broadcast to UI
|
||||||
const deviceCodePrompt: DeviceCodePrompt = {
|
const deviceCodePrompt: DeviceCodePrompt = {
|
||||||
@@ -525,6 +543,7 @@ export function extractLikelyAuthFailureFromLogs(
|
|||||||
});
|
});
|
||||||
|
|
||||||
const providerPatterns: Partial<Record<CLIProxyProvider, RegExp[]>> = {
|
const providerPatterns: Partial<Record<CLIProxyProvider, RegExp[]>> = {
|
||||||
|
xai: [/xai authentication failed:\s*(.+)/i],
|
||||||
ghcp: [
|
ghcp: [
|
||||||
/github copilot authentication failed:\s*(.+)/i,
|
/github copilot authentication failed:\s*(.+)/i,
|
||||||
/failed to verify copilot access[^:]*:\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',
|
id: 'grok-4.3',
|
||||||
name: 'Grok 4.3',
|
name: 'Grok 4.3',
|
||||||
description: 'General-purpose Grok model with a one-million-token context window',
|
description: 'General-purpose Grok model with a one-million-token context window',
|
||||||
extendedContext: true,
|
|
||||||
thinking: {
|
thinking: {
|
||||||
type: 'levels',
|
type: 'levels',
|
||||||
levels: ['none', 'low', 'medium', 'high'],
|
levels: ['none', 'low', 'medium', 'high'],
|
||||||
@@ -336,19 +335,16 @@ export const MODEL_CATALOG: Partial<Record<CLIProxyProvider, ProviderCatalog>> =
|
|||||||
id: 'grok-4.20-0309-reasoning',
|
id: 'grok-4.20-0309-reasoning',
|
||||||
name: 'Grok 4.20 0309 Reasoning',
|
name: 'Grok 4.20 0309 Reasoning',
|
||||||
description: 'Reasoning model with a two-million-token context window',
|
description: 'Reasoning model with a two-million-token context window',
|
||||||
extendedContext: true,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'grok-4.20-0309-non-reasoning',
|
id: 'grok-4.20-0309-non-reasoning',
|
||||||
name: 'Grok 4.20 0309 Non Reasoning',
|
name: 'Grok 4.20 0309 Non Reasoning',
|
||||||
description: 'Non-reasoning model with a two-million-token context window',
|
description: 'Non-reasoning model with a two-million-token context window',
|
||||||
extendedContext: true,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'grok-4.20-multi-agent-0309',
|
id: 'grok-4.20-multi-agent-0309',
|
||||||
name: 'Grok 4.20 Multi Agent 0309',
|
name: 'Grok 4.20 Multi Agent 0309',
|
||||||
description: 'Multi-agent model with a two-million-token context window',
|
description: 'Multi-agent model with a two-million-token context window',
|
||||||
extendedContext: true,
|
|
||||||
thinking: { type: 'levels', levels: ['low', 'medium', 'high'] },
|
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.
|
* Returns true if model has extendedContext: true in catalog.
|
||||||
*/
|
*/
|
||||||
export function supportsExtendedContext(provider: CLIProxyProvider, modelId: string): boolean {
|
export function supportsExtendedContext(provider: CLIProxyProvider, modelId: string): boolean {
|
||||||
|
if (provider === 'xai') return false;
|
||||||
const model = findModel(provider, modelId);
|
const model = findModel(provider, modelId);
|
||||||
return model?.extendedContext === true;
|
return model?.extendedContext === true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -215,13 +215,15 @@ function mapThinking(remote?: RemoteThinkingSupport): ThinkingSupport | undefine
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Map RemoteModelInfo to ModelEntry */
|
/** Map RemoteModelInfo to ModelEntry */
|
||||||
function mapRemoteToModelEntry(remote: RemoteModelInfo): ModelEntry {
|
function mapRemoteToModelEntry(provider: CLIProxyProvider, remote: RemoteModelInfo): ModelEntry {
|
||||||
const entry: ModelEntry = {
|
const entry: ModelEntry = {
|
||||||
id: remote.id,
|
id: remote.id,
|
||||||
name: remote.display_name || remote.id,
|
name: remote.display_name || remote.id,
|
||||||
};
|
};
|
||||||
if (remote.description) entry.description = remote.description;
|
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;
|
entry.extendedContext = true;
|
||||||
}
|
}
|
||||||
const thinking = mapThinking(remote.thinking);
|
const thinking = mapThinking(remote.thinking);
|
||||||
@@ -264,7 +266,7 @@ export function mergeCatalog(
|
|||||||
const mergedModels: ModelEntry[] = [];
|
const mergedModels: ModelEntry[] = [];
|
||||||
|
|
||||||
for (const remote of filteredRemoteModels) {
|
for (const remote of filteredRemoteModels) {
|
||||||
const remoteEntry = mapRemoteToModelEntry(remote);
|
const remoteEntry = mapRemoteToModelEntry(provider, remote);
|
||||||
const staticEntry = staticMap.get(remote.id.toLowerCase());
|
const staticEntry = staticMap.get(remote.id.toLowerCase());
|
||||||
if (staticEntry) {
|
if (staticEntry) {
|
||||||
const mergedThinking = remoteEntry.thinking
|
const mergedThinking = remoteEntry.thinking
|
||||||
|
|||||||
@@ -407,25 +407,21 @@ export const MODEL_CATALOGS: Record<string, ProviderCatalog> = {
|
|||||||
id: 'grok-4.3',
|
id: 'grok-4.3',
|
||||||
name: 'Grok 4.3',
|
name: 'Grok 4.3',
|
||||||
description: 'General-purpose Grok model with a one-million-token context window',
|
description: 'General-purpose Grok model with a one-million-token context window',
|
||||||
extendedContext: true,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'grok-4.20-0309-reasoning',
|
id: 'grok-4.20-0309-reasoning',
|
||||||
name: 'Grok 4.20 0309 Reasoning',
|
name: 'Grok 4.20 0309 Reasoning',
|
||||||
description: 'Reasoning model with a two-million-token context window',
|
description: 'Reasoning model with a two-million-token context window',
|
||||||
extendedContext: true,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'grok-4.20-0309-non-reasoning',
|
id: 'grok-4.20-0309-non-reasoning',
|
||||||
name: 'Grok 4.20 0309 Non Reasoning',
|
name: 'Grok 4.20 0309 Non Reasoning',
|
||||||
description: 'Non-reasoning model with a two-million-token context window',
|
description: 'Non-reasoning model with a two-million-token context window',
|
||||||
extendedContext: true,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'grok-4.20-multi-agent-0309',
|
id: 'grok-4.20-multi-agent-0309',
|
||||||
name: 'Grok 4.20 Multi Agent 0309',
|
name: 'Grok 4.20 Multi Agent 0309',
|
||||||
description: 'Multi-agent model with a two-million-token context window',
|
description: 'Multi-agent model with a two-million-token context window',
|
||||||
extendedContext: true,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'grok-3-mini',
|
id: 'grok-3-mini',
|
||||||
@@ -1124,7 +1120,8 @@ export function buildUiCatalog(
|
|||||||
provider: string,
|
provider: string,
|
||||||
liveCatalog: ProviderCatalog | undefined
|
liveCatalog: ProviderCatalog | undefined
|
||||||
): 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) {
|
if (!liveCatalog || liveCatalog.models.length === 0) {
|
||||||
return staticCatalog;
|
return staticCatalog;
|
||||||
}
|
}
|
||||||
@@ -1145,7 +1142,10 @@ export function buildUiCatalog(
|
|||||||
issueUrl: staticModel?.issueUrl,
|
issueUrl: staticModel?.issueUrl,
|
||||||
deprecated: staticModel?.deprecated,
|
deprecated: staticModel?.deprecated,
|
||||||
deprecationReason: staticModel?.deprecationReason,
|
deprecationReason: staticModel?.deprecationReason,
|
||||||
extendedContext: model.extendedContext ?? staticModel?.extendedContext,
|
extendedContext:
|
||||||
|
normalizedProvider === 'xai'
|
||||||
|
? undefined
|
||||||
|
: (model.extendedContext ?? staticModel?.extendedContext),
|
||||||
presetMapping: staticModel?.presetMapping,
|
presetMapping: staticModel?.presetMapping,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -1344,5 +1344,6 @@ export function supportsExtendedContext(
|
|||||||
modelId: string,
|
modelId: string,
|
||||||
catalogOverride?: ProviderCatalog
|
catalogOverride?: ProviderCatalog
|
||||||
): boolean {
|
): boolean {
|
||||||
|
if (provider.toLowerCase() === 'xai') return false;
|
||||||
return findCatalogModel(provider, modelId, catalogOverride)?.extendedContext === true;
|
return findCatalogModel(provider, modelId, catalogOverride)?.extendedContext === true;
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, expect, it } from 'vitest';
|
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', () => {
|
describe('xAI model catalog defaults', () => {
|
||||||
it('mirrors the CLIProxyAPI text catalog and default tier routing', () => {
|
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',
|
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