mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-13 14:15:56 +00:00
feat(cliproxy): delegate kiro/ghcp token refresh to CLIProxyAPIPlus (#488)
CLIProxyAPIPlus already handles background token refresh for kiro (every 1 min via background_refresh.go) and other providers. CCS was incorrectly returning "not implemented" errors for these providers, causing noisy worker logs and preventing proper token expiry tracking. Changes: - Classify providers into CCS-managed (gemini), CLIProxy-delegated (codex, agy, kiro, ghcp, qwen, iflow), and not-implemented (claude) - Delegated providers return success with delegated flag instead of error - Skip refresh_token requirement for delegated providers in expiry checker - Filter delegated providers from background worker refresh loop Closes #487
This commit is contained in:
1 parent
8172c65d1e
commit
215c00e983
4 files changed
+55
-10
No files matched your search
@@ -2,7 +2,12 @@
|
|||||||
* Provider Token Refreshers
|
* Provider Token Refreshers
|
||||||
*
|
*
|
||||||
* Exports refresh functions for each OAuth provider.
|
* Exports refresh functions for each OAuth provider.
|
||||||
* Currently only Gemini is implemented; others return placeholder errors.
|
*
|
||||||
|
* Refresh responsibility:
|
||||||
|
* - CCS-managed: gemini (CCS refreshes tokens directly via Google OAuth)
|
||||||
|
* - CLIProxy-delegated: codex, agy, kiro, ghcp, qwen, iflow
|
||||||
|
* (CLIProxyAPIPlus handles refresh automatically in background)
|
||||||
|
* - Not implemented: claude
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { CLIProxyProvider } from '../../types';
|
import { CLIProxyProvider } from '../../types';
|
||||||
@@ -13,6 +18,29 @@ export interface ProviderRefreshResult {
|
|||||||
success: boolean;
|
success: boolean;
|
||||||
error?: string;
|
error?: string;
|
||||||
expiresAt?: number;
|
expiresAt?: number;
|
||||||
|
/** True if refresh is delegated to CLIProxy (not handled by CCS) */
|
||||||
|
delegated?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Providers where CLIProxyAPIPlus owns token refresh.
|
||||||
|
* CLIProxyAPIPlus runs background refresh automatically (e.g. kiro: every 1 min).
|
||||||
|
* CCS should not attempt to refresh these — just trust CLIProxy.
|
||||||
|
*/
|
||||||
|
const CLIPROXY_DELEGATED_REFRESH: CLIProxyProvider[] = [
|
||||||
|
'codex',
|
||||||
|
'agy',
|
||||||
|
'kiro',
|
||||||
|
'ghcp',
|
||||||
|
'qwen',
|
||||||
|
'iflow',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a provider's token refresh is delegated to CLIProxy
|
||||||
|
*/
|
||||||
|
export function isRefreshDelegated(provider: CLIProxyProvider): boolean {
|
||||||
|
return CLIPROXY_DELEGATED_REFRESH.includes(provider);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -35,6 +63,10 @@ export async function refreshToken(
|
|||||||
case 'iflow':
|
case 'iflow':
|
||||||
case 'kiro':
|
case 'kiro':
|
||||||
case 'ghcp':
|
case 'ghcp':
|
||||||
|
// CLIProxyAPIPlus handles refresh for these providers automatically.
|
||||||
|
// No action needed from CCS — report success with delegated flag.
|
||||||
|
return { success: true, delegated: true };
|
||||||
|
|
||||||
case 'claude':
|
case 'claude':
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import * as path from 'path';
|
|||||||
import { CLIProxyProvider } from '../types';
|
import { CLIProxyProvider } from '../types';
|
||||||
import { CLIPROXY_PROFILES } from '../../auth/profile-detector';
|
import { CLIPROXY_PROFILES } from '../../auth/profile-detector';
|
||||||
import { getProviderAccounts, getAccountTokenPath } from '../account-manager';
|
import { getProviderAccounts, getAccountTokenPath } from '../account-manager';
|
||||||
|
import { isRefreshDelegated } from './provider-refreshers';
|
||||||
|
|
||||||
/** Preemptive refresh time: refresh tokens 45 minutes before expiry */
|
/** Preemptive refresh time: refresh tokens 45 minutes before expiry */
|
||||||
export const PREEMPTIVE_REFRESH_MINUTES = 45;
|
export const PREEMPTIVE_REFRESH_MINUTES = 45;
|
||||||
@@ -74,9 +75,13 @@ export function getTokenExpiryInfo(
|
|||||||
const content = fs.readFileSync(tokenPath, 'utf-8');
|
const content = fs.readFileSync(tokenPath, 'utf-8');
|
||||||
const data: TokenData = JSON.parse(content);
|
const data: TokenData = JSON.parse(content);
|
||||||
|
|
||||||
// Validate refresh_token exists (required for refresh)
|
// Validate refresh_token exists (required for CCS-managed refresh).
|
||||||
if (!data.refresh_token || typeof data.refresh_token !== 'string') {
|
// CLIProxy-delegated providers may not expose refresh_token in token files —
|
||||||
return null;
|
// CLIProxyAPIPlus manages refresh internally, so skip this check for them.
|
||||||
|
if (!isRefreshDelegated(provider)) {
|
||||||
|
if (!data.refresh_token || typeof data.refresh_token !== 'string') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate expiry time with validation
|
// Calculate expiry time with validation
|
||||||
|
|||||||
@@ -317,6 +317,13 @@ export function displayAuthStatus(): void {
|
|||||||
* Ensure OAuth token is valid for provider, refreshing if expired or expiring soon.
|
* Ensure OAuth token is valid for provider, refreshing if expired or expiring soon.
|
||||||
* This prevents UND_ERR_SOCKET errors caused by expired tokens during API calls.
|
* This prevents UND_ERR_SOCKET errors caused by expired tokens during API calls.
|
||||||
*
|
*
|
||||||
|
* Refresh responsibility:
|
||||||
|
* - gemini: CCS refreshes directly via Google OAuth
|
||||||
|
* - codex, agy, kiro, ghcp, qwen, iflow: CLIProxyAPIPlus handles refresh
|
||||||
|
* automatically in background (e.g. kiro refreshes every 1 min).
|
||||||
|
* CCS only checks if token file exists (authentication state).
|
||||||
|
* - claude: not yet implemented
|
||||||
|
*
|
||||||
* @param provider The CLIProxy provider
|
* @param provider The CLIProxy provider
|
||||||
* @param verbose Log progress if true
|
* @param verbose Log progress if true
|
||||||
* @returns Object with valid status and whether refresh occurred
|
* @returns Object with valid status and whether refresh occurred
|
||||||
@@ -325,14 +332,12 @@ export async function ensureTokenValid(
|
|||||||
provider: CLIProxyProvider,
|
provider: CLIProxyProvider,
|
||||||
verbose = false
|
verbose = false
|
||||||
): Promise<{ valid: boolean; refreshed: boolean; error?: string }> {
|
): Promise<{ valid: boolean; refreshed: boolean; error?: string }> {
|
||||||
// Currently only Gemini uses oauth_creds.json with expiry_date
|
|
||||||
// Other providers (agy, codex) use CLIProxyAPI's internal auth management
|
|
||||||
if (provider === 'gemini') {
|
if (provider === 'gemini') {
|
||||||
const { ensureGeminiTokenValid } = await import('./gemini-token-refresh');
|
const { ensureGeminiTokenValid } = await import('./gemini-token-refresh');
|
||||||
return ensureGeminiTokenValid(verbose);
|
return ensureGeminiTokenValid(verbose);
|
||||||
}
|
}
|
||||||
|
|
||||||
// For other providers, assume token is valid if authenticated
|
// For CLIProxy-delegated providers, token refresh is handled by CLIProxyAPIPlus.
|
||||||
// CLIProxyAPI handles token refresh internally for antigravity/codex
|
// CCS only verifies the token file exists (authentication state).
|
||||||
return { valid: isAuthenticated(provider), refreshed: false };
|
return { valid: isAuthenticated(provider), refreshed: false };
|
||||||
}
|
}
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
import { CLIProxyProvider } from '../types';
|
import { CLIProxyProvider } from '../types';
|
||||||
import { getAllTokenExpiryInfo, TokenExpiryInfo } from './token-expiry-checker';
|
import { getAllTokenExpiryInfo, TokenExpiryInfo } from './token-expiry-checker';
|
||||||
import { refreshToken } from './provider-refreshers';
|
import { refreshToken, isRefreshDelegated } from './provider-refreshers';
|
||||||
|
|
||||||
/** Worker configuration */
|
/** Worker configuration */
|
||||||
export interface TokenRefreshConfig {
|
export interface TokenRefreshConfig {
|
||||||
@@ -182,7 +182,10 @@ export class TokenRefreshWorker {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const tokens = getAllTokenExpiryInfo();
|
const tokens = getAllTokenExpiryInfo();
|
||||||
const tokensNeedingRefresh = tokens.filter((t) => t.needsRefresh);
|
// Skip CLIProxy-delegated providers — they handle their own refresh
|
||||||
|
const tokensNeedingRefresh = tokens.filter(
|
||||||
|
(t) => t.needsRefresh && !isRefreshDelegated(t.provider)
|
||||||
|
);
|
||||||
|
|
||||||
if (tokensNeedingRefresh.length === 0) {
|
if (tokensNeedingRefresh.length === 0) {
|
||||||
this.log('[OK] All tokens valid, no refresh needed');
|
this.log('[OK] All tokens valid, no refresh needed');
|
||||||
|
|||||||
Reference in new issue
Block a user