mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-12 18:15:48 +00:00
feat(cliproxy): add xAI Grok provider
This commit is contained in:
1 parent
1b67c7840b
commit
54a2b4759b
37 files changed
+510
-54
No files matched your search
@@ -50,6 +50,7 @@ Then launch whatever runtime fits the task:
|
|||||||
```bash
|
```bash
|
||||||
ccs
|
ccs
|
||||||
ccs codex
|
ccs codex
|
||||||
|
ccs xai # Alias: ccs grok
|
||||||
ccs --target droid glm
|
ccs --target droid glm
|
||||||
ccs glm
|
ccs glm
|
||||||
ccs ollama
|
ccs ollama
|
||||||
@@ -61,7 +62,7 @@ CCS gives you one stable command surface while letting you switch between:
|
|||||||
|
|
||||||
- multiple runtimes such as Claude Code, Factory Droid, and Codex CLI
|
- multiple runtimes such as Claude Code, Factory Droid, and Codex CLI
|
||||||
- multiple Claude subscriptions and isolated account contexts
|
- multiple Claude subscriptions and isolated account contexts
|
||||||
- OAuth providers like Codex, Kiro, Claude, Kimi, and more, with legacy
|
- OAuth providers like Codex, xAI/Grok, Kiro, Claude, Kimi, and more, with legacy
|
||||||
Copilot compatibility for existing setups
|
Copilot compatibility for existing setups
|
||||||
- API and local-model profiles like GLM, Kimi, OpenRouter, Ollama, llama.cpp,
|
- API and local-model profiles like GLM, Kimi, OpenRouter, Ollama, llama.cpp,
|
||||||
Novita, Fireworks AI, and Alibaba Coding Plan
|
Novita, Fireworks AI, and Alibaba Coding Plan
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"env": {
|
||||||
|
"ANTHROPIC_BASE_URL": "http://127.0.0.1:8317/api/provider/xai",
|
||||||
|
"ANTHROPIC_AUTH_TOKEN": "ccs-internal-managed",
|
||||||
|
"ANTHROPIC_MODEL": "grok-build-0.1",
|
||||||
|
"ANTHROPIC_DEFAULT_OPUS_MODEL": "grok-4.5",
|
||||||
|
"ANTHROPIC_DEFAULT_SONNET_MODEL": "grok-build-0.1",
|
||||||
|
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "grok-composer-2.5-fast"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -31,7 +31,7 @@ Developers using Claude Code face these challenges:
|
|||||||
CCS provides:
|
CCS provides:
|
||||||
|
|
||||||
1. **Multi-Account Claude**: Isolated instances via `CLAUDE_CONFIG_DIR`
|
1. **Multi-Account Claude**: Isolated instances via `CLAUDE_CONFIG_DIR`
|
||||||
2. **OAuth Providers**: Zero-config Gemini, Codex, Antigravity, Kiro, and other active OAuth integrations, with deprecated Copilot compatibility for existing setups
|
2. **OAuth Providers**: Zero-config Gemini, Codex, xAI/Grok, Antigravity, Kiro, and other active OAuth integrations, with deprecated Copilot compatibility for existing setups
|
||||||
3. **AI Providers**: Dedicated CLIProxy dashboard for Gemini, Codex, Claude, Vertex, and OpenAI-compatible API-key families
|
3. **AI Providers**: Dedicated CLIProxy dashboard for Gemini, Codex, Claude, Vertex, and OpenAI-compatible API-key families
|
||||||
4. **API Profiles**: GLM, Kimi, OpenRouter, any Anthropic-compatible API
|
4. **API Profiles**: GLM, Kimi, OpenRouter, any Anthropic-compatible API
|
||||||
5. **Visual Dashboard**: React SPA for configuration management
|
5. **Visual Dashboard**: React SPA for configuration management
|
||||||
@@ -68,8 +68,8 @@ CCS provides:
|
|||||||
- Share commands, skills, agents across accounts
|
- Share commands, skills, agents across accounts
|
||||||
|
|
||||||
### FR-003: OAuth Provider Integration
|
### FR-003: OAuth Provider Integration
|
||||||
- Support Gemini, Codex, Antigravity, Kiro, and deprecated Copilot compatibility OAuth flows
|
- Support Gemini, Codex, xAI/Grok, Antigravity, Kiro, and deprecated Copilot compatibility OAuth flows
|
||||||
- Browser-based authentication (Authorization Code flow for most, Device Code for ghcp compatibility)
|
- Browser-based authentication with provider-specific Authorization Code, Device Code, or polling flows
|
||||||
- Token caching and refresh
|
- Token caching and refresh
|
||||||
|
|
||||||
### FR-004: API Profile Management
|
### FR-004: API Profile Management
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Provider Integration Flows
|
# Provider Integration Flows
|
||||||
|
|
||||||
Last Updated: 2026-05-07
|
Last Updated: 2026-07-16
|
||||||
|
|
||||||
Detailed provider integration flows including CLIProxyAPI, legacy GLMT compatibility transforms, remote CLIProxy, quota management, and authentication.
|
Detailed provider integration flows including CLIProxyAPI, legacy GLMT compatibility transforms, remote CLIProxy, quota management, and authentication.
|
||||||
|
|
||||||
@@ -35,13 +35,13 @@ Generated local CLIProxy configs also keep the management dashboard aligned with
|
|||||||
+---> OAuth Authentication
|
+---> OAuth Authentication
|
||||||
| |
|
| |
|
||||||
| +---> Authorization Code Flow (port-based)
|
| +---> Authorization Code Flow (port-based)
|
||||||
| | - Gemini, Codex, Antigravity, Kiro (port 9876)
|
| | - Gemini, Codex, Antigravity, iFlow, Claude, GitLab
|
||||||
| | - Opens browser for user auth
|
| | - Opens browser for user auth
|
||||||
| | - Callback to localhost:PORT
|
| | - Callback to localhost:PORT
|
||||||
| |
|
| |
|
||||||
| +---> Device Code Flow (no port needed)
|
| +---> Device Code Flow (no port needed)
|
||||||
| - GitHub Copilot (ghcp, deprecated compatibility)
|
| - xAI/Grok, Kimi, Kiro, Copilot, CodeBuddy, Kilo, and Qoder
|
||||||
| - User enters code at github.com/login/device
|
| - User enters a code on the provider verification page
|
||||||
| - Polls for token completion
|
| - Polls for token completion
|
||||||
| |
|
| |
|
||||||
| +---> Browser URL Polling (no callback port)
|
| +---> Browser URL Polling (no callback port)
|
||||||
@@ -70,23 +70,37 @@ Generated local CLIProxy configs also keep the management dashboard aligned with
|
|||||||
+---> Provider APIs
|
+---> Provider APIs
|
||||||
|
|
|
|
||||||
+---> Google (Gemini)
|
+---> Google (Gemini)
|
||||||
+---> GitHub (Codex)
|
+---> OpenAI (Codex)
|
||||||
|
+---> xAI (Grok)
|
||||||
+---> Antigravity (AGY)
|
+---> Antigravity (AGY)
|
||||||
+---> AWS Kiro (Claude-powered)
|
+---> AWS Kiro (Claude-powered)
|
||||||
+---> GitHub Copilot (ghcp, deprecated compatibility)
|
+---> GitHub Copilot (ghcp, deprecated compatibility)
|
||||||
+---> OpenAI-compatible endpoints
|
+---> OpenAI-compatible endpoints
|
||||||
```
|
```
|
||||||
|
|
||||||
### Supported Hardcoded Providers
|
### Supported Built-In Providers
|
||||||
|
|
||||||
| Provider | ID | Auth Method | Port | Binary |
|
| Provider | ID | Auth Method | Callback Port | Backend |
|
||||||
|----------|----|----|------|--------|
|
|----------|----|----|------|--------|
|
||||||
| Gemini | `gemini` | Authorization Code | 9876 | CLIProxyAPI |
|
| Gemini | `gemini` | Authorization Code | 8085 | Original |
|
||||||
| Codex | `codex` | Authorization Code | 9876 | CLIProxyAPI |
|
| Codex | `codex` | Authorization Code | 1455 | Original |
|
||||||
| Antigravity | `agy` | Authorization Code | 9876 | CLIProxyAPI |
|
| xAI (Grok) | `xai` (`grok` CLI alias) | Device Code | none | Original |
|
||||||
| Kiro (AWS) | `kiro` | Method-aware (default: Device Code) | 9876 | CLIProxyAPIPlus fork |
|
| Antigravity | `agy` | Authorization Code | 51121 | Original |
|
||||||
| GitHub Copilot (deprecated) | `ghcp` | Device Code | none | CLIProxyAPIPlus fork |
|
| Qwen | `qwen` | Account linking unavailable in bundled runtime | none | Original |
|
||||||
| Cursor | `cursor` | Browser URL polling | none | CLIProxyAPIPlus fork |
|
| iFlow | `iflow` | Authorization Code | 11451 | Original |
|
||||||
|
| Claude | `claude` | Authorization Code | 54545 | Original |
|
||||||
|
| Kimi | `kimi` | Device Code | none | Original |
|
||||||
|
| Kiro (AWS) | `kiro` | Method-aware (default: Device Code) | none by default | Plus |
|
||||||
|
| GitHub Copilot (deprecated) | `ghcp` | Device Code | none | Plus |
|
||||||
|
| Cursor | `cursor` | Browser URL polling | none | Plus |
|
||||||
|
| GitLab Duo | `gitlab` | Authorization Code or PAT | 17171 | Plus |
|
||||||
|
| CodeBuddy | `codebuddy` | Device-style polling | none | Plus |
|
||||||
|
| Kilo AI | `kilo` | Device Code | none | Plus |
|
||||||
|
| Qoder | `qoder` | Device Code | none | Plus |
|
||||||
|
|
||||||
|
xAI uses CLIProxyAPI's `--xai-login` device flow. CCS stores and discovers the resulting
|
||||||
|
`xai-*.json` credentials under the canonical `xai` provider. `ccs grok` is a command alias only,
|
||||||
|
so it shares the same accounts, settings, live model catalog, and routing as `ccs xai`.
|
||||||
|
|
||||||
### Codex Duplicate-Email Account Identity
|
### Codex Duplicate-Email Account Identity
|
||||||
|
|
||||||
@@ -100,16 +114,16 @@ Codex can legitimately produce multiple auth files for the same email when the u
|
|||||||
|
|
||||||
This preserves the user-visible distinction between business and personal Codex sessions while keeping other providers on their existing email-backed identity model.
|
This preserves the user-visible distinction between business and personal Codex sessions while keeping other providers on their existing email-backed identity model.
|
||||||
|
|
||||||
### Hardcoded Provider Detection
|
### Built-In Provider Detection
|
||||||
|
|
||||||
CCS detects hardcoded providers via `profile-detector.ts` and routes through `execClaudeWithCLIProxy()`.
|
CCS derives canonical providers from `provider-capabilities.ts`. `profile-detector.ts` resolves only
|
||||||
|
explicit CLI aliases before routing through the CLIProxy execution flow.
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// Profile name matching
|
const provider = resolveCLIProxyProviderShortcut(profileName);
|
||||||
const hardcodedProviders = ['gemini', 'codex', 'agy', 'kiro', 'ghcp'];
|
|
||||||
|
|
||||||
if (hardcodedProviders.includes(profileName)) {
|
if (provider) {
|
||||||
return execClaudeWithCLIProxy(claudeCli, profileName, args);
|
return execClaudeWithCLIProxy(claudeCli, provider, args);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,10 @@ import {
|
|||||||
|
|
||||||
import { getProfileLookupCandidates, isLegacyProfileAlias } from '../utils/profile-compat';
|
import { getProfileLookupCandidates, isLegacyProfileAlias } from '../utils/profile-compat';
|
||||||
import type { CLIProxyProvider } from '../cliproxy/types';
|
import type { CLIProxyProvider } from '../cliproxy/types';
|
||||||
import { CLIPROXY_PROVIDER_IDS, isCLIProxyProvider } from '../cliproxy/provider-capabilities';
|
import {
|
||||||
|
CLIPROXY_PROVIDER_IDS,
|
||||||
|
resolveCLIProxyProviderShortcut,
|
||||||
|
} from '../cliproxy/provider-capabilities';
|
||||||
import { LEGACY_CURSOR_PROFILE_NAME } from '../cursor/constants';
|
import { LEGACY_CURSOR_PROFILE_NAME } from '../cursor/constants';
|
||||||
import { normalizeCopilotModelId } from '../copilot/copilot-model-normalizer';
|
import { normalizeCopilotModelId } from '../copilot/copilot-model-normalizer';
|
||||||
import type { TargetType } from '../targets/target-adapter';
|
import type { TargetType } from '../targets/target-adapter';
|
||||||
@@ -332,11 +335,12 @@ class ProfileDetector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Priority 0.5: Check CLIProxy profiles (gemini, codex, agy, qwen, ...)
|
// Priority 0.5: Check CLIProxy profiles (gemini, codex, agy, qwen, ...)
|
||||||
if (isCLIProxyProvider(profileName)) {
|
const builtinProvider = resolveCLIProxyProviderShortcut(profileName);
|
||||||
|
if (builtinProvider) {
|
||||||
return {
|
return {
|
||||||
type: 'cliproxy',
|
type: 'cliproxy',
|
||||||
name: profileName,
|
name: builtinProvider,
|
||||||
provider: profileName,
|
provider: builtinProvider,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -37,11 +37,30 @@ describe('model-catalog compatibility lookups', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('maps all dashboard providers to upstream catalog channels', () => {
|
it('maps all dashboard providers to upstream catalog channels', () => {
|
||||||
|
expect(SYNCABLE_PROVIDERS).toContain('xai');
|
||||||
expect(SYNCABLE_PROVIDERS).toContain('qwen');
|
expect(SYNCABLE_PROVIDERS).toContain('qwen');
|
||||||
expect(SYNCABLE_PROVIDERS).toContain('iflow');
|
expect(SYNCABLE_PROVIDERS).toContain('iflow');
|
||||||
expect(SYNCABLE_PROVIDERS).toContain('kiro');
|
expect(SYNCABLE_PROVIDERS).toContain('kiro');
|
||||||
expect(SYNCABLE_PROVIDERS).toContain('ghcp');
|
expect(SYNCABLE_PROVIDERS).toContain('ghcp');
|
||||||
expect(PROVIDER_TO_CHANNEL.ghcp).toBe('github-copilot');
|
expect(PROVIDER_TO_CHANNEL.ghcp).toBe('github-copilot');
|
||||||
|
expect(PROVIDER_TO_CHANNEL.xai).toBe('xai');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('merges live xAI model metadata while preserving the static coding default', () => {
|
||||||
|
const catalog = mergeCatalog('xai', [
|
||||||
|
{ id: 'grok-build-0.1', display_name: 'Grok Build 0.1' },
|
||||||
|
{
|
||||||
|
id: 'grok-4.5',
|
||||||
|
display_name: 'Grok 4.5',
|
||||||
|
thinking: { levels: ['low', 'medium', 'high'] },
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(catalog?.defaultModel).toBe('grok-build-0.1');
|
||||||
|
expect(catalog?.models[1]?.thinking).toMatchObject({
|
||||||
|
type: 'levels',
|
||||||
|
levels: ['low', 'medium', 'high'],
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
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', () => {
|
||||||
|
|||||||
@@ -63,6 +63,32 @@ describe('Model Catalog', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('xAI models', () => {
|
||||||
|
it('contains the xAI provider catalog with the coding default', () => {
|
||||||
|
const { MODEL_CATALOG } = modelCatalog;
|
||||||
|
assert(MODEL_CATALOG.xai, 'Should have xai provider');
|
||||||
|
assert.strictEqual(MODEL_CATALOG.xai.provider, 'xai');
|
||||||
|
assert.strictEqual(MODEL_CATALOG.xai.displayName, 'xAI (Grok)');
|
||||||
|
assert.strictEqual(MODEL_CATALOG.xai.defaultModel, 'grok-build-0.1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('matches the CLIProxyAPI text-model fallback set', () => {
|
||||||
|
const { MODEL_CATALOG } = modelCatalog;
|
||||||
|
const ids = MODEL_CATALOG.xai.models.map((model) => model.id);
|
||||||
|
assert.deepStrictEqual(ids, [
|
||||||
|
'grok-build-0.1',
|
||||||
|
'grok-4.5',
|
||||||
|
'grok-4.3',
|
||||||
|
'grok-4.20-0309-reasoning',
|
||||||
|
'grok-4.20-0309-non-reasoning',
|
||||||
|
'grok-4.20-multi-agent-0309',
|
||||||
|
'grok-3-mini',
|
||||||
|
'grok-3-mini-fast',
|
||||||
|
'grok-composer-2.5-fast',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('AGY models', () => {
|
describe('AGY models', () => {
|
||||||
it('has correct default model', () => {
|
it('has correct default model', () => {
|
||||||
const { MODEL_CATALOG } = modelCatalog;
|
const { MODEL_CATALOG } = modelCatalog;
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
getProvidersByOAuthFlow,
|
getProvidersByOAuthFlow,
|
||||||
isCLIProxyProvider,
|
isCLIProxyProvider,
|
||||||
mapExternalProviderName,
|
mapExternalProviderName,
|
||||||
|
resolveCLIProxyProviderShortcut,
|
||||||
QUOTA_SUPPORTED_PROVIDER_IDS,
|
QUOTA_SUPPORTED_PROVIDER_IDS,
|
||||||
isQuotaSupportedProvider,
|
isQuotaSupportedProvider,
|
||||||
QUOTA_PROVIDER_OPTION_VALUES,
|
QUOTA_PROVIDER_OPTION_VALUES,
|
||||||
@@ -25,6 +26,7 @@ import {
|
|||||||
getKiroCallbackPort,
|
getKiroCallbackPort,
|
||||||
getKiroCLIAuthArgs,
|
getKiroCLIAuthArgs,
|
||||||
getKiroCLIAuthFlag,
|
getKiroCLIAuthFlag,
|
||||||
|
getOAuthConfig,
|
||||||
normalizeKiroIDCFlow,
|
normalizeKiroIDCFlow,
|
||||||
normalizeKiroAuthMethod,
|
normalizeKiroAuthMethod,
|
||||||
OAUTH_CALLBACK_PORTS as AUTH_CALLBACK_PORTS,
|
OAUTH_CALLBACK_PORTS as AUTH_CALLBACK_PORTS,
|
||||||
@@ -36,6 +38,7 @@ describe('provider-capabilities', () => {
|
|||||||
expect(CLIPROXY_PROVIDER_IDS).toEqual([
|
expect(CLIPROXY_PROVIDER_IDS).toEqual([
|
||||||
'gemini',
|
'gemini',
|
||||||
'codex',
|
'codex',
|
||||||
|
'xai',
|
||||||
'agy',
|
'agy',
|
||||||
'qwen',
|
'qwen',
|
||||||
'iflow',
|
'iflow',
|
||||||
@@ -61,6 +64,7 @@ describe('provider-capabilities', () => {
|
|||||||
|
|
||||||
it('returns providers by OAuth flow capability', () => {
|
it('returns providers by OAuth flow capability', () => {
|
||||||
expect(getProvidersByOAuthFlow('device_code')).toEqual([
|
expect(getProvidersByOAuthFlow('device_code')).toEqual([
|
||||||
|
'xai',
|
||||||
'qwen',
|
'qwen',
|
||||||
'kiro',
|
'kiro',
|
||||||
'ghcp',
|
'ghcp',
|
||||||
@@ -82,6 +86,7 @@ describe('provider-capabilities', () => {
|
|||||||
|
|
||||||
it('separates browser URL auth providers from verification-code device flows', () => {
|
it('separates browser URL auth providers from verification-code device flows', () => {
|
||||||
expect(getDeviceCodeVerificationProviders()).toEqual([
|
expect(getDeviceCodeVerificationProviders()).toEqual([
|
||||||
|
'xai',
|
||||||
'qwen',
|
'qwen',
|
||||||
'kiro',
|
'kiro',
|
||||||
'ghcp',
|
'ghcp',
|
||||||
@@ -101,11 +106,20 @@ describe('provider-capabilities', () => {
|
|||||||
expect(mapExternalProviderName('anthropic')).toBe('claude');
|
expect(mapExternalProviderName('anthropic')).toBe('claude');
|
||||||
expect(mapExternalProviderName('gitlab-duo')).toBe('gitlab');
|
expect(mapExternalProviderName('gitlab-duo')).toBe('gitlab');
|
||||||
expect(mapExternalProviderName('tencent')).toBe('codebuddy');
|
expect(mapExternalProviderName('tencent')).toBe('codebuddy');
|
||||||
|
expect(mapExternalProviderName('grok')).toBe('xai');
|
||||||
|
expect(mapExternalProviderName('x.ai')).toBe('xai');
|
||||||
expect(mapExternalProviderName(' COPILOT ')).toBe('ghcp');
|
expect(mapExternalProviderName(' COPILOT ')).toBe('ghcp');
|
||||||
expect(mapExternalProviderName('')).toBeNull();
|
expect(mapExternalProviderName('')).toBeNull();
|
||||||
expect(mapExternalProviderName('unknown-provider')).toBeNull();
|
expect(mapExternalProviderName('unknown-provider')).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('keeps CLI aliases explicit and canonical', () => {
|
||||||
|
expect(resolveCLIProxyProviderShortcut('xai')).toBe('xai');
|
||||||
|
expect(resolveCLIProxyProviderShortcut('grok')).toBe('xai');
|
||||||
|
expect(resolveCLIProxyProviderShortcut(' GROK ')).toBe('xai');
|
||||||
|
expect(resolveCLIProxyProviderShortcut('anthropic')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
it('exposes quota-supported providers and guards correctly', () => {
|
it('exposes quota-supported providers and guards correctly', () => {
|
||||||
expect(QUOTA_SUPPORTED_PROVIDER_IDS).toEqual(['agy', 'codex', 'claude', 'gemini', 'ghcp']);
|
expect(QUOTA_SUPPORTED_PROVIDER_IDS).toEqual(['agy', 'codex', 'claude', 'gemini', 'ghcp']);
|
||||||
expect(QUOTA_PROVIDER_OPTION_VALUES).toEqual([
|
expect(QUOTA_PROVIDER_OPTION_VALUES).toEqual([
|
||||||
@@ -130,6 +144,7 @@ describe('provider-capabilities', () => {
|
|||||||
|
|
||||||
it('exposes callback port and display name capabilities', () => {
|
it('exposes callback port and display name capabilities', () => {
|
||||||
expect(getOAuthCallbackPort('qwen')).toBeNull();
|
expect(getOAuthCallbackPort('qwen')).toBeNull();
|
||||||
|
expect(getOAuthCallbackPort('xai')).toBeNull();
|
||||||
expect(getOAuthCallbackPort('kiro')).toBeNull();
|
expect(getOAuthCallbackPort('kiro')).toBeNull();
|
||||||
expect(getOAuthCallbackPort('cursor')).toBeNull();
|
expect(getOAuthCallbackPort('cursor')).toBeNull();
|
||||||
expect(getOAuthCallbackPort('gitlab')).toBe(17171);
|
expect(getOAuthCallbackPort('gitlab')).toBe(17171);
|
||||||
@@ -138,6 +153,7 @@ describe('provider-capabilities', () => {
|
|||||||
expect(PROVIDER_CAPABILITIES.qwen.refreshOwnership).toBe('unsupported');
|
expect(PROVIDER_CAPABILITIES.qwen.refreshOwnership).toBe('unsupported');
|
||||||
expect(getProviderDisplayName('agy')).toBe('Antigravity');
|
expect(getProviderDisplayName('agy')).toBe('Antigravity');
|
||||||
expect(getProviderDisplayName('kilo')).toBe('Kilo AI');
|
expect(getProviderDisplayName('kilo')).toBe('Kilo AI');
|
||||||
|
expect(getProviderDisplayName('xai')).toBe('xAI (Grok)');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('exposes auth start support separately from OAuth flow type', () => {
|
it('exposes auth start support separately from OAuth flow type', () => {
|
||||||
@@ -147,6 +163,15 @@ describe('provider-capabilities', () => {
|
|||||||
);
|
);
|
||||||
expect(getUnsupportedAuthStartReason('kiro')).toBeNull();
|
expect(getUnsupportedAuthStartReason('kiro')).toBeNull();
|
||||||
expect(getUnsupportedAuthStartReason('qoder')).toBeNull();
|
expect(getUnsupportedAuthStartReason('qoder')).toBeNull();
|
||||||
|
expect(getUnsupportedAuthStartReason('xai')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses the CLIProxyAPI xAI OAuth command contract', () => {
|
||||||
|
expect(getOAuthConfig('xai')).toMatchObject({
|
||||||
|
provider: 'xai',
|
||||||
|
authFlag: '--xai-login',
|
||||||
|
});
|
||||||
|
expect(AUTH_CALLBACK_PORTS.xai).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throws when provider aliases collide across providers', () => {
|
it('throws when provider aliases collide across providers', () => {
|
||||||
|
|||||||
@@ -128,6 +128,36 @@ describe('Account Manager - discoverExistingAccounts', () => {
|
|||||||
assert(accounts.providers.ghcp, 'copilot type should map to ghcp provider');
|
assert(accounts.providers.ghcp, 'copilot type should map to ghcp provider');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('discovers xAI accounts with type="xai"', () => {
|
||||||
|
createAuthFile('xai-user@example.com.json', {
|
||||||
|
type: 'xai',
|
||||||
|
email: 'user@example.com',
|
||||||
|
});
|
||||||
|
|
||||||
|
accountManager.discoverExistingAccounts();
|
||||||
|
const accounts = getAccountsFile();
|
||||||
|
|
||||||
|
assert(accounts.providers.xai, 'xai type should map to xai provider');
|
||||||
|
assert(accounts.providers.xai.accounts['user@example.com']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps multiple xAI accounts distinct when token metadata has no email', () => {
|
||||||
|
createAuthFile('xai-subject-one.json', {
|
||||||
|
type: 'xai',
|
||||||
|
});
|
||||||
|
createAuthFile('xai-subject-two.json', {
|
||||||
|
type: 'xai',
|
||||||
|
});
|
||||||
|
|
||||||
|
accountManager.discoverExistingAccounts();
|
||||||
|
const accounts = getAccountsFile();
|
||||||
|
|
||||||
|
assert.deepStrictEqual(Object.keys(accounts.providers.xai.accounts).sort(), [
|
||||||
|
'subject-one',
|
||||||
|
'subject-two',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
it('handles case-insensitive type values', () => {
|
it('handles case-insensitive type values', () => {
|
||||||
createAuthFile('kiro-test.json', {
|
createAuthFile('kiro-test.json', {
|
||||||
type: 'KIRO',
|
type: 'KIRO',
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export type AccountTier = 'free' | 'pro' | 'ultra' | 'unknown';
|
|||||||
* Providers that typically have empty email in OAuth token files.
|
* Providers that typically have empty email in OAuth token files.
|
||||||
* For these providers, nickname is used as accountId instead of email.
|
* For these providers, nickname is used as accountId instead of email.
|
||||||
*/
|
*/
|
||||||
export const PROVIDERS_WITHOUT_EMAIL: CLIProxyProvider[] = ['kiro', 'ghcp'];
|
export const PROVIDERS_WITHOUT_EMAIL: CLIProxyProvider[] = ['kiro', 'ghcp', 'xai'];
|
||||||
|
|
||||||
/** Account information */
|
/** Account information */
|
||||||
export interface AccountInfo {
|
export interface AccountInfo {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ describe('auth-types paste-callback start path', () => {
|
|||||||
'/v0/management/gemini-cli-auth-url?is_webui=true'
|
'/v0/management/gemini-cli-auth-url?is_webui=true'
|
||||||
);
|
);
|
||||||
expect(getPasteCallbackStartPath('codex')).toBe('/v0/management/codex-auth-url?is_webui=true');
|
expect(getPasteCallbackStartPath('codex')).toBe('/v0/management/codex-auth-url?is_webui=true');
|
||||||
|
expect(getPasteCallbackStartPath('xai')).toBe('/v0/management/xai-auth-url?is_webui=true');
|
||||||
expect(getPasteCallbackStartPath('agy')).toBe(
|
expect(getPasteCallbackStartPath('agy')).toBe(
|
||||||
'/v0/management/antigravity-auth-url?is_webui=true'
|
'/v0/management/antigravity-auth-url?is_webui=true'
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { describe, expect, it } from 'bun:test';
|
||||||
|
import { OAUTH_CALLBACK_PORTS } from '../../../management/oauth-port-diagnostics';
|
||||||
|
import { buildOAuthArgs } from '../oauth-handler';
|
||||||
|
|
||||||
|
describe('xAI OAuth process arguments', () => {
|
||||||
|
it('starts CLIProxy device auth without a callback port argument', () => {
|
||||||
|
expect(OAUTH_CALLBACK_PORTS.xai).toBeNull();
|
||||||
|
expect(buildOAuthArgs('xai', '/tmp/cliproxy-config.yaml', false, false)).toEqual([
|
||||||
|
'--config',
|
||||||
|
'/tmp/cliproxy-config.yaml',
|
||||||
|
'--xai-login',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses no-browser mode in headless environments', () => {
|
||||||
|
expect(buildOAuthArgs('xai', '/tmp/cliproxy-config.yaml', true, false)).toContain(
|
||||||
|
'--no-browser'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -4,11 +4,28 @@ import {
|
|||||||
extractLikelyAuthFailureFromLogs,
|
extractLikelyAuthFailureFromLogs,
|
||||||
extractLikelyAuthFailureFromStderr,
|
extractLikelyAuthFailureFromStderr,
|
||||||
extractLikelyOAuthAuthorizationUrl,
|
extractLikelyOAuthAuthorizationUrl,
|
||||||
|
extractDeviceCodePrompt,
|
||||||
getExpectedLocalCallback,
|
getExpectedLocalCallback,
|
||||||
getKiroBuilderIdSelectionInput,
|
getKiroBuilderIdSelectionInput,
|
||||||
validateManualCallbackUrl,
|
validateManualCallbackUrl,
|
||||||
} from '../oauth-process';
|
} from '../oauth-process';
|
||||||
|
|
||||||
|
describe('oauth-process device code parsing', () => {
|
||||||
|
it('parses the xAI device authorization output contract', () => {
|
||||||
|
const output = `
|
||||||
|
To authenticate, please visit:
|
||||||
|
https://accounts.x.ai/oauth2/device?user_code=ABCD-1234
|
||||||
|
|
||||||
|
Then enter this code: ABCD-1234
|
||||||
|
`;
|
||||||
|
|
||||||
|
expect(extractDeviceCodePrompt(output)).toEqual({
|
||||||
|
userCode: 'ABCD-1234',
|
||||||
|
verificationUrl: 'https://accounts.x.ai/oauth2/device?user_code=ABCD-1234',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('oauth-process stderr parsing', () => {
|
describe('oauth-process stderr parsing', () => {
|
||||||
it('does not match provider-specific patterns for other providers', () => {
|
it('does not match provider-specific patterns for other providers', () => {
|
||||||
const stderr =
|
const stderr =
|
||||||
|
|||||||
@@ -149,6 +149,7 @@ export function toKiroManagementMethod(method: KiroAuthMethod): 'aws' | 'google'
|
|||||||
* OAuth flow types per provider:
|
* OAuth flow types per provider:
|
||||||
* - Gemini: Authorization Code Flow with local callback server on port 8085
|
* - Gemini: Authorization Code Flow with local callback server on port 8085
|
||||||
* - Codex: Authorization Code Flow with local callback server on port 1455
|
* - Codex: Authorization Code Flow with local callback server on port 1455
|
||||||
|
* - xAI: Device Code Flow (polling-based, NO callback port needed)
|
||||||
* - Agy: Authorization Code Flow with local callback server on port 51121
|
* - Agy: Authorization Code Flow with local callback server on port 51121
|
||||||
* - iFlow: Authorization Code Flow with local callback server on port 11451
|
* - iFlow: Authorization Code Flow with local callback server on port 11451
|
||||||
* - Claude: Authorization Code Flow with local callback server on port 54545 (Anthropic OAuth)
|
* - Claude: Authorization Code Flow with local callback server on port 54545 (Anthropic OAuth)
|
||||||
@@ -228,6 +229,13 @@ export const OAUTH_CONFIGS: Record<CLIProxyProvider, ProviderOAuthConfig> = {
|
|||||||
scopes: ['openid', 'profile'],
|
scopes: ['openid', 'profile'],
|
||||||
authFlag: '--codex-login',
|
authFlag: '--codex-login',
|
||||||
},
|
},
|
||||||
|
xai: {
|
||||||
|
provider: 'xai',
|
||||||
|
displayName: 'xAI (Grok)',
|
||||||
|
authUrl: 'https://accounts.x.ai/oauth2/device',
|
||||||
|
scopes: ['openid', 'profile', 'email', 'offline_access', 'grok-cli:access', 'api:access'],
|
||||||
|
authFlag: '--xai-login',
|
||||||
|
},
|
||||||
agy: {
|
agy: {
|
||||||
provider: 'agy',
|
provider: 'agy',
|
||||||
displayName: 'Antigravity',
|
displayName: 'Antigravity',
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* OAuth Handler for CLIProxyAPI
|
* OAuth Handler for CLIProxyAPI
|
||||||
*
|
*
|
||||||
* Manages OAuth authentication flow for CLIProxy providers (Gemini, Codex, Antigravity, Kiro, Copilot).
|
* Manages OAuth authentication flow for CLIProxy providers such as Gemini, Codex, xAI, and Kiro.
|
||||||
* CLIProxyAPI handles OAuth internally - we just need to:
|
* CLIProxyAPI handles OAuth internally - we just need to:
|
||||||
* 1. Check if auth exists (token files in CCS auth directory)
|
* 1. Check if auth exists (token files in CCS auth directory)
|
||||||
* 2. Trigger OAuth flow by spawning binary with auth flag
|
* 2. Trigger OAuth flow by spawning binary with auth flag
|
||||||
@@ -605,7 +605,7 @@ async function prepareBinary(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildOAuthArgs(
|
export function buildOAuthArgs(
|
||||||
provider: CLIProxyProvider,
|
provider: CLIProxyProvider,
|
||||||
configPath: string,
|
configPath: string,
|
||||||
headless: boolean,
|
headless: boolean,
|
||||||
|
|||||||
@@ -127,6 +127,22 @@ function resolveAuthFlowType(options: OAuthProcessOptions): 'device_code' | 'aut
|
|||||||
return options.authFlowType || OAUTH_FLOW_TYPES[options.provider] || 'authorization_code';
|
return options.authFlowType || OAUTH_FLOW_TYPES[options.provider] || 'authorization_code';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function extractDeviceCodePrompt(output: string): {
|
||||||
|
userCode: string;
|
||||||
|
verificationUrl: string | null;
|
||||||
|
} | null {
|
||||||
|
const codeMatch = output.match(
|
||||||
|
/(?:enter\s+)?(?:this\s+)?(?:user\s+)?code[:\s]+["']?([A-Z0-9]{4,8}[-\s]?[A-Z0-9]{4,8})["']?/i
|
||||||
|
);
|
||||||
|
if (!codeMatch) return null;
|
||||||
|
|
||||||
|
const urlMatch = output.match(/(https?:\/\/[^\s]+device[^\s]*)/i);
|
||||||
|
return {
|
||||||
|
userCode: codeMatch[1].toUpperCase(),
|
||||||
|
verificationUrl: urlMatch?.[1] ?? null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
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 (
|
||||||
@@ -392,19 +408,14 @@ async function handleStdout(
|
|||||||
|
|
||||||
// Handle Device Code Flow: parse and display user code
|
// Handle Device Code Flow: parse and display user code
|
||||||
if (isDeviceCodeFlow && !state.deviceCodeDisplayed) {
|
if (isDeviceCodeFlow && !state.deviceCodeDisplayed) {
|
||||||
// Parse device/user code from various formats:
|
const devicePrompt = extractDeviceCodePrompt(state.accumulatedOutput);
|
||||||
// "Enter code: XXXX-YYYY" or "code XXXX-YYYY" or "user code: XXXX-YYYY"
|
|
||||||
const codeMatch = state.accumulatedOutput.match(
|
|
||||||
/(?:enter\s+)?(?:user\s+)?code[:\s]+["']?([A-Z0-9]{4,8}[-\s]?[A-Z0-9]{4,8})["']?/i
|
|
||||||
);
|
|
||||||
const urlMatch = state.accumulatedOutput.match(/(https?:\/\/[^\s]+device[^\s]*)/i);
|
|
||||||
|
|
||||||
if (codeMatch) {
|
if (devicePrompt) {
|
||||||
state.userCode = codeMatch[1].toUpperCase();
|
state.userCode = devicePrompt.userCode;
|
||||||
state.deviceCodeDisplayed = true;
|
state.deviceCodeDisplayed = true;
|
||||||
log(`Parsed device code: ${state.userCode}`);
|
log(`Parsed device code: ${state.userCode}`);
|
||||||
|
|
||||||
const verificationUrl = urlMatch?.[1] || 'https://github.com/login/device';
|
const verificationUrl = devicePrompt.verificationUrl || 'https://github.com/login/device';
|
||||||
|
|
||||||
// Emit device code event for WebSocket broadcast to UI
|
// Emit device code event for WebSocket broadcast to UI
|
||||||
const deviceCodePrompt: DeviceCodePrompt = {
|
const deviceCodePrompt: DeviceCodePrompt = {
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
* Exports refresh functions for each OAuth provider.
|
* Exports refresh functions for each OAuth provider.
|
||||||
*
|
*
|
||||||
* Refresh responsibility:
|
* Refresh responsibility:
|
||||||
* - CLIProxy-delegated: gemini, codex, agy, kiro, ghcp, iflow, kimi
|
* - CLIProxy-delegated: gemini, codex, xai, agy, kiro, ghcp, iflow, kimi
|
||||||
* (CLIProxyAPIPlus handles refresh automatically in background)
|
* (the selected CLIProxy backend handles refresh automatically in background)
|
||||||
* - Unsupported account linking: qwen
|
* - Unsupported account linking: qwen
|
||||||
* - Not implemented: claude
|
* - Not implemented: claude
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ describe('base-config-loader new providers', () => {
|
|||||||
['codebuddy', '/api/provider/codebuddy', 'auto'],
|
['codebuddy', '/api/provider/codebuddy', 'auto'],
|
||||||
['kilo', '/api/provider/kilo', 'kilo/auto'],
|
['kilo', '/api/provider/kilo', 'kilo/auto'],
|
||||||
['qoder', '/api/provider/qoder', 'qoder/auto'],
|
['qoder', '/api/provider/qoder', 'qoder/auto'],
|
||||||
|
['xai', '/api/provider/xai', 'grok-build-0.1'],
|
||||||
] as const)('loads base settings for %s', (provider, baseUrlPath, defaultModel) => {
|
] as const)('loads base settings for %s', (provider, baseUrlPath, defaultModel) => {
|
||||||
const config = loadBaseConfig(provider);
|
const config = loadBaseConfig(provider);
|
||||||
|
|
||||||
|
|||||||
@@ -698,7 +698,7 @@ routing:
|
|||||||
|
|
||||||
// Unified config with enhanced CLIProxyAPI features
|
// Unified config with enhanced CLIProxyAPI features
|
||||||
const config = `# CLIProxyAPI config generated by CCS v${CLIPROXY_CONFIG_VERSION}
|
const config = `# CLIProxyAPI config generated by CCS v${CLIPROXY_CONFIG_VERSION}
|
||||||
# Supports: gemini, codex, agy, qwen, iflow (concurrent usage)
|
# Supports: gemini, codex, xai, agy, qwen, iflow, and other registered providers (concurrent usage)
|
||||||
# Generated: ${new Date().toISOString()}
|
# Generated: ${new Date().toISOString()}
|
||||||
#
|
#
|
||||||
# This config is auto-managed by CCS. Manual edits may be overwritten.
|
# This config is auto-managed by CCS. Manual edits may be overwritten.
|
||||||
|
|||||||
@@ -301,6 +301,75 @@ export const MODEL_CATALOG: Partial<Record<CLIProxyProvider, ProviderCatalog>> =
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
xai: {
|
||||||
|
provider: 'xai',
|
||||||
|
displayName: 'xAI (Grok)',
|
||||||
|
defaultModel: 'grok-build-0.1',
|
||||||
|
models: [
|
||||||
|
{
|
||||||
|
id: 'grok-build-0.1',
|
||||||
|
name: 'Grok Build 0.1',
|
||||||
|
description: 'Fast coding model for agentic software engineering workflows',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'grok-4.5',
|
||||||
|
name: 'Grok 4.5',
|
||||||
|
description: 'Frontier model for coding, engineering, and agentic workflows',
|
||||||
|
thinking: {
|
||||||
|
type: 'levels',
|
||||||
|
levels: ['low', 'medium', 'high'],
|
||||||
|
zeroAllowed: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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'],
|
||||||
|
zeroAllowed: 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,
|
||||||
|
thinking: { type: 'levels', levels: ['low', 'medium', 'high'] },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'grok-3-mini',
|
||||||
|
name: 'Grok 3 Mini',
|
||||||
|
description: 'Compact reasoning model',
|
||||||
|
thinking: { type: 'levels', levels: ['low', 'medium', 'high'] },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'grok-3-mini-fast',
|
||||||
|
name: 'Grok 3 Mini Fast',
|
||||||
|
description: 'Faster compact reasoning model',
|
||||||
|
thinking: { type: 'levels', levels: ['low', 'medium', 'high'] },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'grok-composer-2.5-fast',
|
||||||
|
name: 'Grok Composer 2.5 Fast',
|
||||||
|
description: 'Fast Composer model for lightweight coding tasks',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
qoder: {
|
qoder: {
|
||||||
provider: 'qoder',
|
provider: 'qoder',
|
||||||
displayName: 'Qoder',
|
displayName: 'Qoder',
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ export interface ProviderCapabilities {
|
|||||||
authFilePrefixes: readonly string[];
|
authFilePrefixes: readonly string[];
|
||||||
/** Token JSON "type" values accepted for this provider. */
|
/** Token JSON "type" values accepted for this provider. */
|
||||||
tokenTypeValues: readonly string[];
|
tokenTypeValues: readonly string[];
|
||||||
|
/** Alternative names accepted as first-class CCS CLI shortcuts. */
|
||||||
|
cliAliases?: readonly string[];
|
||||||
/**
|
/**
|
||||||
* Alternative provider names used by CLIProxyAPI or stats endpoints.
|
* Alternative provider names used by CLIProxyAPI or stats endpoints.
|
||||||
* These aliases normalize external names to canonical CCS provider IDs.
|
* These aliases normalize external names to canonical CCS provider IDs.
|
||||||
@@ -58,6 +60,20 @@ export const PROVIDER_CAPABILITIES: Record<CLIProxyProvider, ProviderCapabilitie
|
|||||||
tokenTypeValues: ['codex'],
|
tokenTypeValues: ['codex'],
|
||||||
aliases: [],
|
aliases: [],
|
||||||
},
|
},
|
||||||
|
xai: {
|
||||||
|
displayName: 'xAI (Grok)',
|
||||||
|
description: 'Grok coding and reasoning models',
|
||||||
|
oauthFlow: 'device_code',
|
||||||
|
callbackPort: null,
|
||||||
|
callbackProviderName: 'xai',
|
||||||
|
authUrlProviderName: 'xai',
|
||||||
|
refreshOwnership: 'cliproxy',
|
||||||
|
authStartSupport: 'cliproxy-cli',
|
||||||
|
authFilePrefixes: ['xai-'],
|
||||||
|
tokenTypeValues: ['xai'],
|
||||||
|
cliAliases: ['grok'],
|
||||||
|
aliases: ['grok', 'x-ai', 'x.ai'],
|
||||||
|
},
|
||||||
agy: {
|
agy: {
|
||||||
displayName: 'Antigravity',
|
displayName: 'Antigravity',
|
||||||
description: 'Antigravity AI models',
|
description: 'Antigravity AI models',
|
||||||
@@ -296,10 +312,40 @@ export function buildProviderAliasMap(
|
|||||||
|
|
||||||
const PROVIDER_ALIAS_MAP: ReadonlyMap<string, CLIProxyProvider> = buildProviderAliasMap();
|
const PROVIDER_ALIAS_MAP: ReadonlyMap<string, CLIProxyProvider> = buildProviderAliasMap();
|
||||||
|
|
||||||
|
const PROVIDER_CLI_SHORTCUT_MAP: ReadonlyMap<string, CLIProxyProvider> = (() => {
|
||||||
|
const shortcutMap = new Map<string, CLIProxyProvider>();
|
||||||
|
for (const provider of CLIPROXY_PROVIDER_IDS) {
|
||||||
|
shortcutMap.set(provider, provider);
|
||||||
|
for (const alias of PROVIDER_CAPABILITIES[provider].cliAliases ?? []) {
|
||||||
|
const normalized = alias.trim().toLowerCase();
|
||||||
|
if (!normalized) continue;
|
||||||
|
const existingProvider = shortcutMap.get(normalized);
|
||||||
|
if (existingProvider && existingProvider !== provider) {
|
||||||
|
throw new ConfigError(
|
||||||
|
`Provider CLI alias collision for "${normalized}": ${existingProvider} and ${provider}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
shortcutMap.set(normalized, provider);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return shortcutMap;
|
||||||
|
})();
|
||||||
|
|
||||||
export function isCLIProxyProvider(provider: string): provider is CLIProxyProvider {
|
export function isCLIProxyProvider(provider: string): provider is CLIProxyProvider {
|
||||||
return PROVIDER_ID_SET.has(provider as CLIProxyProvider);
|
return PROVIDER_ID_SET.has(provider as CLIProxyProvider);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Resolve canonical provider IDs and explicitly supported CLI shortcut aliases. */
|
||||||
|
export function resolveCLIProxyProviderShortcut(providerName: string): CLIProxyProvider | null {
|
||||||
|
const normalized = providerName.trim().toLowerCase();
|
||||||
|
if (!normalized) return null;
|
||||||
|
return PROVIDER_CLI_SHORTCUT_MAP.get(normalized) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getProviderCLIAliases(provider: CLIProxyProvider): readonly string[] {
|
||||||
|
return PROVIDER_CAPABILITIES[provider].cliAliases ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
export function getProviderCapabilities(provider: CLIProxyProvider): ProviderCapabilities {
|
export function getProviderCapabilities(provider: CLIProxyProvider): ProviderCapabilities {
|
||||||
return PROVIDER_CAPABILITIES[provider];
|
return PROVIDER_CAPABILITIES[provider];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ const CHANNEL_TO_PROVIDER: Record<string, CLIProxyProvider> = {
|
|||||||
claude: 'claude',
|
claude: 'claude',
|
||||||
gemini: 'gemini',
|
gemini: 'gemini',
|
||||||
codex: 'codex',
|
codex: 'codex',
|
||||||
|
xai: 'xai',
|
||||||
qwen: 'qwen',
|
qwen: 'qwen',
|
||||||
iflow: 'iflow',
|
iflow: 'iflow',
|
||||||
kimi: 'kimi',
|
kimi: 'kimi',
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
export type CLIProxyProvider =
|
export type CLIProxyProvider =
|
||||||
| 'gemini'
|
| 'gemini'
|
||||||
| 'codex'
|
| 'codex'
|
||||||
|
| 'xai'
|
||||||
| 'agy'
|
| 'agy'
|
||||||
| 'qwen'
|
| 'qwen'
|
||||||
| 'iflow'
|
| 'iflow'
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
import { COPILOT_SUBCOMMANDS } from '../copilot/constants';
|
import { COPILOT_SUBCOMMANDS } from '../copilot/constants';
|
||||||
import { CLIPROXY_PROVIDER_IDS } from '../cliproxy/provider-capabilities';
|
import {
|
||||||
|
CLIPROXY_PROVIDER_IDS,
|
||||||
|
getProviderCLIAliases,
|
||||||
|
getProviderDisplayName,
|
||||||
|
} from '../cliproxy/provider-capabilities';
|
||||||
|
|
||||||
export type HelpTopicName =
|
export type HelpTopicName =
|
||||||
| 'profiles'
|
| 'profiles'
|
||||||
@@ -200,6 +204,7 @@ export const BUILTIN_PROVIDER_SHORTCUTS: readonly ShortcutEntry[] = CLIPROXY_PRO
|
|||||||
{
|
{
|
||||||
gemini: 'Google Gemini via CLIProxy OAuth',
|
gemini: 'Google Gemini via CLIProxy OAuth',
|
||||||
codex: 'OpenAI Codex via CLIProxy OAuth',
|
codex: 'OpenAI Codex via CLIProxy OAuth',
|
||||||
|
xai: 'xAI Grok via CLIProxy OAuth (alias: grok)',
|
||||||
agy: 'Antigravity via CLIProxy OAuth',
|
agy: 'Antigravity via CLIProxy OAuth',
|
||||||
qwen: 'Qwen Code via CLIProxy; account linking unsupported',
|
qwen: 'Qwen Code via CLIProxy; account linking unsupported',
|
||||||
iflow: 'iFlow via CLIProxy OAuth',
|
iflow: 'iFlow via CLIProxy OAuth',
|
||||||
@@ -216,6 +221,15 @@ export const BUILTIN_PROVIDER_SHORTCUTS: readonly ShortcutEntry[] = CLIPROXY_PRO
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/** Additional top-level CLI aliases; canonical provider lists remain alias-free. */
|
||||||
|
export const BUILTIN_PROVIDER_ALIAS_SHORTCUTS: readonly ShortcutEntry[] =
|
||||||
|
CLIPROXY_PROVIDER_IDS.flatMap((provider) =>
|
||||||
|
getProviderCLIAliases(provider).map((name) => ({
|
||||||
|
name,
|
||||||
|
summary: `Alias for ${getProviderDisplayName(provider)} provider shortcut`,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
export const ROOT_PROFILE_EXAMPLES: readonly ShortcutEntry[] = [
|
export const ROOT_PROFILE_EXAMPLES: readonly ShortcutEntry[] = [
|
||||||
{ name: 'ccs auth create work', summary: 'Create a concurrent Claude account profile' },
|
{ name: 'ccs auth create work', summary: 'Create a concurrent Claude account profile' },
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import ProfileDetector from '../auth/profile-detector';
|
|||||||
import {
|
import {
|
||||||
API_SUBCOMMANDS,
|
API_SUBCOMMANDS,
|
||||||
AUTH_SUBCOMMANDS,
|
AUTH_SUBCOMMANDS,
|
||||||
|
BUILTIN_PROVIDER_ALIAS_SHORTCUTS,
|
||||||
BUILTIN_PROVIDER_SHORTCUTS,
|
BUILTIN_PROVIDER_SHORTCUTS,
|
||||||
CLEANUP_FLAGS,
|
CLEANUP_FLAGS,
|
||||||
COMMAND_FLAG_SUGGESTIONS,
|
COMMAND_FLAG_SUGGESTIONS,
|
||||||
@@ -65,6 +66,7 @@ function getTopLevelSuggestions(): CompletionSuggestion[] {
|
|||||||
...getPublicRootCommandTokens().map((value) => suggestion(value)),
|
...getPublicRootCommandTokens().map((value) => suggestion(value)),
|
||||||
...ROOT_COMMAND_FLAGS.map((value) => suggestion(value)),
|
...ROOT_COMMAND_FLAGS.map((value) => suggestion(value)),
|
||||||
...BUILTIN_PROVIDER_SHORTCUTS.map((entry) => suggestion(entry.name, entry.summary)),
|
...BUILTIN_PROVIDER_SHORTCUTS.map((entry) => suggestion(entry.name, entry.summary)),
|
||||||
|
...BUILTIN_PROVIDER_ALIAS_SHORTCUTS.map((entry) => suggestion(entry.name, entry.summary)),
|
||||||
...getDynamicProfileSuggestions(),
|
...getDynamicProfileSuggestions(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -262,6 +264,7 @@ function getSuggestionsForCommand(tokensBeforeCurrent: string[]): CompletionSugg
|
|||||||
...getProfileNames('settings'),
|
...getProfileNames('settings'),
|
||||||
...getProfileNames('accounts'),
|
...getProfileNames('accounts'),
|
||||||
...BUILTIN_PROVIDER_SHORTCUTS.map((entry) => entry.name),
|
...BUILTIN_PROVIDER_SHORTCUTS.map((entry) => entry.name),
|
||||||
|
...BUILTIN_PROVIDER_ALIAS_SHORTCUTS.map((entry) => entry.name),
|
||||||
]),
|
]),
|
||||||
COMMAND_FLAG_SUGGESTIONS.env
|
COMMAND_FLAG_SUGGESTIONS.env
|
||||||
);
|
);
|
||||||
@@ -280,7 +283,11 @@ function getSuggestionsForCommand(tokensBeforeCurrent: string[]): CompletionSugg
|
|||||||
['--bash', '--zsh', '--fish', '--powershell', '--force', '-f']
|
['--bash', '--zsh', '--fish', '--powershell', '--force', '-f']
|
||||||
);
|
);
|
||||||
default:
|
default:
|
||||||
if (BUILTIN_PROVIDER_SHORTCUTS.some((entry) => entry.name === command)) {
|
if (
|
||||||
|
[...BUILTIN_PROVIDER_SHORTCUTS, ...BUILTIN_PROVIDER_ALIAS_SHORTCUTS].some(
|
||||||
|
(entry) => entry.name === command
|
||||||
|
)
|
||||||
|
) {
|
||||||
if (command === 'agy')
|
if (command === 'agy')
|
||||||
return completeSubcommands([], [...PROVIDER_FLAGS, '--accept-agr-risk']);
|
return completeSubcommands([], [...PROVIDER_FLAGS, '--accept-agr-risk']);
|
||||||
if (command === 'kiro') {
|
if (command === 'kiro') {
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ export function generateYamlWithComments(config: UnifiedConfig): string {
|
|||||||
|
|
||||||
// CLIProxy section
|
// CLIProxy section
|
||||||
lines.push('# ----------------------------------------------------------------------------');
|
lines.push('# ----------------------------------------------------------------------------');
|
||||||
lines.push('# CLIProxy: OAuth-based providers (gemini, codex, agy, qwen, iflow)');
|
lines.push('# CLIProxy: OAuth-based providers (gemini, codex, xai/grok, agy, qwen, iflow)');
|
||||||
lines.push('# Each variant can reference a *.settings.json file for custom env vars.');
|
lines.push('# Each variant can reference a *.settings.json file for custom env vars.');
|
||||||
lines.push('# Edit the settings file directly to customize model or other settings.');
|
lines.push('# Edit the settings file directly to customize model or other settings.');
|
||||||
lines.push(
|
lines.push(
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { info } from '../utils/ui';
|
import { info } from '../utils/ui';
|
||||||
import { isCLIProxyProvider } from '../cliproxy/provider-capabilities';
|
import { resolveCLIProxyProviderShortcut } from '../cliproxy/provider-capabilities';
|
||||||
import { isCopilotSubcommandToken } from '../copilot/constants';
|
import { isCopilotSubcommandToken } from '../copilot/constants';
|
||||||
import { isCursorSubcommandToken, LEGACY_CURSOR_PROFILE_NAME } from '../cursor/constants';
|
import { isCursorSubcommandToken, LEGACY_CURSOR_PROFILE_NAME } from '../cursor/constants';
|
||||||
import { isCacheStale } from '../utils/update-checker';
|
import { isCacheStale } from '../utils/update-checker';
|
||||||
@@ -111,14 +111,11 @@ export async function runPreDispatchHandlers(ctx: PreDispatchContext): Promise<b
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Provider help shortcut: `ccs gemini --help` → provider-specific help page
|
// Provider help shortcut: `ccs gemini --help` → provider-specific help page
|
||||||
if (
|
const shortcutProvider =
|
||||||
typeof firstArg === 'string' &&
|
typeof firstArg === 'string' ? resolveCLIProxyProviderShortcut(firstArg) : null;
|
||||||
isCLIProxyProvider(firstArg) &&
|
if (shortcutProvider && args.length > 1 && (args.includes('--help') || args.includes('-h'))) {
|
||||||
args.length > 1 &&
|
|
||||||
(args.includes('--help') || args.includes('-h'))
|
|
||||||
) {
|
|
||||||
const { showProviderShortcutHelp } = await import('../commands/help-command');
|
const { showProviderShortcutHelp } = await import('../commands/help-command');
|
||||||
await showProviderShortcutHelp(firstArg);
|
await showProviderShortcutHelp(shortcutProvider);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -94,12 +94,20 @@ describe('ProfileDetector', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should detect newly added cliproxy providers', () => {
|
it('should detect newly added cliproxy providers', () => {
|
||||||
|
expect(detector.detectProfileType('xai').provider).toBe('xai');
|
||||||
expect(detector.detectProfileType('gitlab').provider).toBe('gitlab');
|
expect(detector.detectProfileType('gitlab').provider).toBe('gitlab');
|
||||||
expect(detector.detectProfileType('codebuddy').provider).toBe('codebuddy');
|
expect(detector.detectProfileType('codebuddy').provider).toBe('codebuddy');
|
||||||
expect(detector.detectProfileType('kilo').provider).toBe('kilo');
|
expect(detector.detectProfileType('kilo').provider).toBe('kilo');
|
||||||
expect(detector.detectProfileType('qoder').provider).toBe('qoder');
|
expect(detector.detectProfileType('qoder').provider).toBe('qoder');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should resolve the grok CLI alias to canonical xai routing', () => {
|
||||||
|
const result = detector.detectProfileType('grok');
|
||||||
|
expect(result.type).toBe('cliproxy');
|
||||||
|
expect(result.name).toBe('xai');
|
||||||
|
expect(result.provider).toBe('xai');
|
||||||
|
});
|
||||||
|
|
||||||
it('should detect settings-based profile from unified config', () => {
|
it('should detect settings-based profile from unified config', () => {
|
||||||
const settingsPath = path.join(tempDir, 'glm.settings.json');
|
const settingsPath = path.join(tempDir, 'glm.settings.json');
|
||||||
fs.writeFileSync(settingsPath, JSON.stringify({ env: { ANTHROPIC_MODEL: 'glm-4' } }));
|
fs.writeFileSync(settingsPath, JSON.stringify({ env: { ANTHROPIC_MODEL: 'glm-4' } }));
|
||||||
|
|||||||
@@ -82,6 +82,8 @@ describe('completion backend', () => {
|
|||||||
expect(values).toContain('cursor');
|
expect(values).toContain('cursor');
|
||||||
expect(values).toContain('copilot');
|
expect(values).toContain('copilot');
|
||||||
expect(values).toContain('gemini');
|
expect(values).toContain('gemini');
|
||||||
|
expect(values).toContain('xai');
|
||||||
|
expect(values).toContain('grok');
|
||||||
expect(values).toContain('gitlab');
|
expect(values).toContain('gitlab');
|
||||||
expect(values).toContain('codebuddy');
|
expect(values).toContain('codebuddy');
|
||||||
expect(values).toContain('kilo');
|
expect(values).toContain('kilo');
|
||||||
@@ -94,6 +96,11 @@ describe('completion backend', () => {
|
|||||||
expect(values).not.toContain('--uninstall');
|
expect(values).not.toContain('--uninstall');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('offers provider flags for both xai and grok shortcuts', () => {
|
||||||
|
expect(suggestionValues(['xai'])).toEqual(expect.arrayContaining(['--auth', '--accounts']));
|
||||||
|
expect(suggestionValues(['grok'])).toEqual(expect.arrayContaining(['--auth', '--accounts']));
|
||||||
|
});
|
||||||
|
|
||||||
test('suggests help topics from the help command', () => {
|
test('suggests help topics from the help command', () => {
|
||||||
const values = suggestionValues(['help']);
|
const values = suggestionValues(['help']);
|
||||||
expect(values).toContain('profiles');
|
expect(values).toContain('profiles');
|
||||||
|
|||||||
@@ -57,6 +57,8 @@ describe('help command parity', () => {
|
|||||||
expect(rendered.includes('ccs help kiro')).toBe(true);
|
expect(rendered.includes('ccs help kiro')).toBe(true);
|
||||||
expect(rendered.includes('gemini')).toBe(true);
|
expect(rendered.includes('gemini')).toBe(true);
|
||||||
expect(rendered.includes('codex')).toBe(true);
|
expect(rendered.includes('codex')).toBe(true);
|
||||||
|
expect(rendered.includes('xai')).toBe(true);
|
||||||
|
expect(rendered.includes('alias: grok')).toBe(true);
|
||||||
expect(rendered.includes('ghcp')).toBe(true);
|
expect(rendered.includes('ghcp')).toBe(true);
|
||||||
expect(rendered.includes('gitlab')).toBe(true);
|
expect(rendered.includes('gitlab')).toBe(true);
|
||||||
expect(rendered.includes('codebuddy')).toBe(true);
|
expect(rendered.includes('codebuddy')).toBe(true);
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
<title>xAI Grok</title>
|
||||||
|
<path d="M6.469 8.776 16.512 23h-4.464L2.005 8.776H6.47Zm-.004 7.9 2.233 3.164L6.467 23H2l4.465-6.324ZM22 2.582V23h-3.659V7.764L22 2.582ZM22 1l-9.952 14.095-2.233-3.163L17.533 1H22Z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 301 B |
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* Add Account Dialog Component
|
* Add Account Dialog Component
|
||||||
* Uses /start-url to get OAuth URL + polls for completion via management API.
|
* Uses /start-url to get OAuth URL + polls for completion via management API.
|
||||||
* For Device Code flows (ghcp, qwen, kiro): Uses /start endpoint which spawns CLIProxy
|
* For verification-code flows: Uses /start endpoint which spawns CLIProxy
|
||||||
* binary and emits WebSocket events. DeviceCodeDialog handles user code display.
|
* binary and emits WebSocket events. DeviceCodeDialog handles user code display.
|
||||||
* Shows auth URL + callback paste field. Polling auto-closes on success.
|
* Shows auth URL + callback paste field. Polling auto-closes on success.
|
||||||
* For Kiro: Also shows "Import from IDE" option.
|
* For Kiro: Also shows "Import from IDE" option.
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ const WIZARD_PROVIDER_ORDER: CLIProxyProvider[] = [
|
|||||||
'claude',
|
'claude',
|
||||||
'gemini',
|
'gemini',
|
||||||
'codex',
|
'codex',
|
||||||
|
'xai',
|
||||||
'cursor',
|
'cursor',
|
||||||
'gitlab',
|
'gitlab',
|
||||||
'codebuddy',
|
'codebuddy',
|
||||||
|
|||||||
@@ -382,6 +382,68 @@ export const MODEL_CATALOGS: Record<string, ProviderCatalog> = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
xai: {
|
||||||
|
provider: 'xai',
|
||||||
|
displayName: 'xAI (Grok)',
|
||||||
|
defaultModel: 'grok-build-0.1',
|
||||||
|
models: [
|
||||||
|
{
|
||||||
|
id: 'grok-build-0.1',
|
||||||
|
name: 'Grok Build 0.1',
|
||||||
|
description: 'Fast coding model for agentic software engineering workflows',
|
||||||
|
presetMapping: {
|
||||||
|
default: 'grok-build-0.1',
|
||||||
|
opus: 'grok-4.5',
|
||||||
|
sonnet: 'grok-build-0.1',
|
||||||
|
haiku: 'grok-composer-2.5-fast',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'grok-4.5',
|
||||||
|
name: 'Grok 4.5',
|
||||||
|
description: 'Frontier model for coding, engineering, and agentic workflows',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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',
|
||||||
|
name: 'Grok 3 Mini',
|
||||||
|
description: 'Compact reasoning model',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'grok-3-mini-fast',
|
||||||
|
name: 'Grok 3 Mini Fast',
|
||||||
|
description: 'Faster compact reasoning model',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'grok-composer-2.5-fast',
|
||||||
|
name: 'Grok Composer 2.5 Fast',
|
||||||
|
description: 'Fast Composer model for lightweight coding tasks',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
qwen: {
|
qwen: {
|
||||||
provider: 'qwen',
|
provider: 'qwen',
|
||||||
displayName: 'Qwen',
|
displayName: 'Qwen',
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ export const PROVIDER_ASSETS: Partial<Record<ProviderVisualId, string>> = {
|
|||||||
gemini: '/assets/providers/gemini-color.svg',
|
gemini: '/assets/providers/gemini-color.svg',
|
||||||
agy: '/assets/providers/agy.png',
|
agy: '/assets/providers/agy.png',
|
||||||
codex: '/assets/providers/codex-color.svg',
|
codex: '/assets/providers/codex-color.svg',
|
||||||
|
xai: '/assets/providers/xai.svg',
|
||||||
qwen: '/assets/providers/qwen-color.svg',
|
qwen: '/assets/providers/qwen-color.svg',
|
||||||
iflow: '/assets/providers/iflow.png',
|
iflow: '/assets/providers/iflow.png',
|
||||||
kiro: '/assets/providers/kiro.png',
|
kiro: '/assets/providers/kiro.png',
|
||||||
@@ -128,6 +129,7 @@ export const PROVIDER_FALLBACK_VISUALS: Record<ProviderVisualId, ProviderFallbac
|
|||||||
gemini: { textClass: 'text-blue-600', letter: 'G' },
|
gemini: { textClass: 'text-blue-600', letter: 'G' },
|
||||||
claude: { textClass: 'text-orange-600', letter: 'C' },
|
claude: { textClass: 'text-orange-600', letter: 'C' },
|
||||||
codex: { textClass: 'text-emerald-600', letter: 'X' },
|
codex: { textClass: 'text-emerald-600', letter: 'X' },
|
||||||
|
xai: { textClass: 'text-slate-900', letter: 'X' },
|
||||||
agy: { textClass: 'text-violet-600', letter: 'A' },
|
agy: { textClass: 'text-violet-600', letter: 'A' },
|
||||||
qwen: { textClass: 'text-cyan-600', letter: 'Q' },
|
qwen: { textClass: 'text-cyan-600', letter: 'Q' },
|
||||||
iflow: { textClass: 'text-indigo-600', letter: 'i' },
|
iflow: { textClass: 'text-indigo-600', letter: 'i' },
|
||||||
@@ -265,6 +267,7 @@ export const PROVIDER_COLORS: Record<string, string> = {
|
|||||||
gemini: '#4285F4',
|
gemini: '#4285F4',
|
||||||
agy: '#f3722c',
|
agy: '#f3722c',
|
||||||
codex: '#10a37f',
|
codex: '#10a37f',
|
||||||
|
xai: '#111827',
|
||||||
openai: '#111827',
|
openai: '#111827',
|
||||||
vertex: '#4285F4',
|
vertex: '#4285F4',
|
||||||
iflow: '#f94144',
|
iflow: '#f94144',
|
||||||
@@ -370,6 +373,7 @@ const DEVICE_CODE_PROVIDER_INSTRUCTIONS: Readonly<Partial<Record<CLIProxyProvide
|
|||||||
qwen: 'Sign in with your Qwen account to authorize access.',
|
qwen: 'Sign in with your Qwen account to authorize access.',
|
||||||
kiro: 'Sign in with your selected Kiro auth provider to continue.',
|
kiro: 'Sign in with your selected Kiro auth provider to continue.',
|
||||||
kimi: 'Sign in with your Kimi account and finish the device authorization.',
|
kimi: 'Sign in with your Kimi account and finish the device authorization.',
|
||||||
|
xai: 'Sign in with your xAI account and finish the Grok device authorization.',
|
||||||
});
|
});
|
||||||
|
|
||||||
/** Check if the add-account UI should use the verification-code auth dialog. */
|
/** Check if the add-account UI should use the verification-code auth dialog. */
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { MODEL_CATALOGS } from '@/lib/model-catalogs';
|
||||||
|
|
||||||
|
describe('xAI model catalog defaults', () => {
|
||||||
|
it('mirrors the CLIProxyAPI text catalog and default tier routing', () => {
|
||||||
|
const catalog = MODEL_CATALOGS.xai;
|
||||||
|
const ids = catalog.models.map((model) => model.id);
|
||||||
|
const defaultModel = catalog.models.find((model) => model.id === catalog.defaultModel);
|
||||||
|
|
||||||
|
expect(catalog.displayName).toBe('xAI (Grok)');
|
||||||
|
expect(catalog.defaultModel).toBe('grok-build-0.1');
|
||||||
|
expect(ids).toEqual([
|
||||||
|
'grok-build-0.1',
|
||||||
|
'grok-4.5',
|
||||||
|
'grok-4.3',
|
||||||
|
'grok-4.20-0309-reasoning',
|
||||||
|
'grok-4.20-0309-non-reasoning',
|
||||||
|
'grok-4.20-multi-agent-0309',
|
||||||
|
'grok-3-mini',
|
||||||
|
'grok-3-mini-fast',
|
||||||
|
'grok-composer-2.5-fast',
|
||||||
|
]);
|
||||||
|
expect(defaultModel?.presetMapping).toEqual({
|
||||||
|
default: 'grok-build-0.1',
|
||||||
|
opus: 'grok-4.5',
|
||||||
|
sonnet: 'grok-build-0.1',
|
||||||
|
haiku: 'grok-composer-2.5-fast',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest';
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
CLIPROXY_PROVIDER_SECTIONS,
|
CLIPROXY_PROVIDER_SECTIONS,
|
||||||
|
CLIPROXY_PROVIDERS,
|
||||||
CORE_CLIPROXY_PROVIDERS,
|
CORE_CLIPROXY_PROVIDERS,
|
||||||
formatRequestedUpstreamModelRules,
|
formatRequestedUpstreamModelRules,
|
||||||
getProviderDescription,
|
getProviderDescription,
|
||||||
@@ -19,6 +20,7 @@ import {
|
|||||||
PROVIDER_COLORS,
|
PROVIDER_COLORS,
|
||||||
variantUsesPlusExtraProvider,
|
variantUsesPlusExtraProvider,
|
||||||
} from '@/lib/provider-config';
|
} from '@/lib/provider-config';
|
||||||
|
import { PROVIDERS as WIZARD_PROVIDERS } from '@/components/setup/wizard/constants';
|
||||||
|
|
||||||
describe('provider model mapping helpers', () => {
|
describe('provider model mapping helpers', () => {
|
||||||
it('parses requested=upstream rules into stored upstream+alias pairs', () => {
|
it('parses requested=upstream rules into stored upstream+alias pairs', () => {
|
||||||
@@ -61,6 +63,7 @@ describe('provider presentation metadata', () => {
|
|||||||
expect(CLIPROXY_PROVIDER_SECTIONS.map((section) => section.id)).toEqual(['core', 'plus-extra']);
|
expect(CLIPROXY_PROVIDER_SECTIONS.map((section) => section.id)).toEqual(['core', 'plus-extra']);
|
||||||
expect(CORE_CLIPROXY_PROVIDERS).toContain('gemini');
|
expect(CORE_CLIPROXY_PROVIDERS).toContain('gemini');
|
||||||
expect(CORE_CLIPROXY_PROVIDERS).toContain('kimi');
|
expect(CORE_CLIPROXY_PROVIDERS).toContain('kimi');
|
||||||
|
expect(CORE_CLIPROXY_PROVIDERS).toContain('xai');
|
||||||
expect(PLUS_EXTRA_CLIPROXY_PROVIDERS).toEqual([
|
expect(PLUS_EXTRA_CLIPROXY_PROVIDERS).toEqual([
|
||||||
'kiro',
|
'kiro',
|
||||||
'ghcp',
|
'ghcp',
|
||||||
@@ -76,6 +79,12 @@ describe('provider presentation metadata', () => {
|
|||||||
expect(isPlusExtraProvider('gemini')).toBe(false);
|
expect(isPlusExtraProvider('gemini')).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('keeps every non-deprecated canonical provider available in the setup wizard', () => {
|
||||||
|
expect(WIZARD_PROVIDERS.map((provider) => provider.id).sort()).toEqual(
|
||||||
|
CLIPROXY_PROVIDERS.filter((provider) => provider !== 'ghcp').sort()
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('groups provider-backed data by shared section metadata', () => {
|
it('groups provider-backed data by shared section metadata', () => {
|
||||||
const grouped = groupProvidersBySection(
|
const grouped = groupProvidersBySection(
|
||||||
[
|
[
|
||||||
@@ -96,6 +105,7 @@ describe('provider presentation metadata', () => {
|
|||||||
expect(isDeviceCodeProvider('cursor')).toBe(false);
|
expect(isDeviceCodeProvider('cursor')).toBe(false);
|
||||||
expect(isDeviceCodeProvider('ghcp')).toBe(true);
|
expect(isDeviceCodeProvider('ghcp')).toBe(true);
|
||||||
expect(isDeviceCodeProvider('qwen')).toBe(true);
|
expect(isDeviceCodeProvider('qwen')).toBe(true);
|
||||||
|
expect(isDeviceCodeProvider('xai')).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('detects plus-extra providers inside composite variants', () => {
|
it('detects plus-extra providers inside composite variants', () => {
|
||||||
@@ -120,6 +130,7 @@ describe('provider presentation metadata', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it.each([
|
it.each([
|
||||||
|
['xai', 'xAI (Grok)', 'Grok coding and reasoning models', '/assets/providers/xai.svg'],
|
||||||
['cursor', 'Cursor', 'Cursor browser-authenticated provider', '/assets/sidebar/cursor.svg'],
|
['cursor', 'Cursor', 'Cursor browser-authenticated provider', '/assets/sidebar/cursor.svg'],
|
||||||
['gitlab', 'GitLab Duo', 'GitLab Duo with OAuth or PAT auth', '/assets/providers/gitlab.svg'],
|
['gitlab', 'GitLab Duo', 'GitLab Duo with OAuth or PAT auth', '/assets/providers/gitlab.svg'],
|
||||||
[
|
[
|
||||||
@@ -153,10 +164,15 @@ describe('provider presentation metadata', () => {
|
|||||||
textClass: 'text-rose-600',
|
textClass: 'text-rose-600',
|
||||||
letter: 'K',
|
letter: 'K',
|
||||||
});
|
});
|
||||||
|
expect(getProviderFallbackVisual('xai')).toEqual({
|
||||||
|
textClass: 'text-slate-900',
|
||||||
|
letter: 'X',
|
||||||
|
});
|
||||||
|
|
||||||
expect(PROVIDER_COLORS.cursor).toBe('#111827');
|
expect(PROVIDER_COLORS.cursor).toBe('#111827');
|
||||||
expect(PROVIDER_COLORS.gitlab).toBe('#FC6D26');
|
expect(PROVIDER_COLORS.gitlab).toBe('#FC6D26');
|
||||||
expect(PROVIDER_COLORS.codebuddy).toBe('#2563EB');
|
expect(PROVIDER_COLORS.codebuddy).toBe('#2563EB');
|
||||||
expect(PROVIDER_COLORS.kilo).toBe('#E11D48');
|
expect(PROVIDER_COLORS.kilo).toBe('#E11D48');
|
||||||
|
expect(PROVIDER_COLORS.xai).toBe('#111827');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
Reference in new issue
Block a user