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:
Kai (Tam Nhu) Tran
2026-02-07 03:34:14 -05:00
committed by GitHub
parent 8172c65d1e
commit 215c00e983
4 changed files with 55 additions and 10 deletions
+33 -1
View File
@@ -2,7 +2,12 @@
* Provider Token Refreshers
*
* 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';
@@ -13,6 +18,29 @@ export interface ProviderRefreshResult {
success: boolean;
error?: string;
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 'kiro':
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':
return {
success: false,
+8 -3
View File
@@ -10,6 +10,7 @@ import * as path from 'path';
import { CLIProxyProvider } from '../types';
import { CLIPROXY_PROFILES } from '../../auth/profile-detector';
import { getProviderAccounts, getAccountTokenPath } from '../account-manager';
import { isRefreshDelegated } from './provider-refreshers';
/** Preemptive refresh time: refresh tokens 45 minutes before expiry */
export const PREEMPTIVE_REFRESH_MINUTES = 45;
@@ -74,9 +75,13 @@ export function getTokenExpiryInfo(
const content = fs.readFileSync(tokenPath, 'utf-8');
const data: TokenData = JSON.parse(content);
// Validate refresh_token exists (required for refresh)
if (!data.refresh_token || typeof data.refresh_token !== 'string') {
return null;
// Validate refresh_token exists (required for CCS-managed refresh).
// CLIProxy-delegated providers may not expose refresh_token in token files —
// 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
+9 -4
View File
@@ -317,6 +317,13 @@ export function displayAuthStatus(): void {
* 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.
*
* 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 verbose Log progress if true
* @returns Object with valid status and whether refresh occurred
@@ -325,14 +332,12 @@ export async function ensureTokenValid(
provider: CLIProxyProvider,
verbose = false
): 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') {
const { ensureGeminiTokenValid } = await import('./gemini-token-refresh');
return ensureGeminiTokenValid(verbose);
}
// For other providers, assume token is valid if authenticated
// CLIProxyAPI handles token refresh internally for antigravity/codex
// For CLIProxy-delegated providers, token refresh is handled by CLIProxyAPIPlus.
// CCS only verifies the token file exists (authentication state).
return { valid: isAuthenticated(provider), refreshed: false };
}
+5 -2
View File
@@ -7,7 +7,7 @@
import { CLIProxyProvider } from '../types';
import { getAllTokenExpiryInfo, TokenExpiryInfo } from './token-expiry-checker';
import { refreshToken } from './provider-refreshers';
import { refreshToken, isRefreshDelegated } from './provider-refreshers';
/** Worker configuration */
export interface TokenRefreshConfig {
@@ -182,7 +182,10 @@ export class TokenRefreshWorker {
try {
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) {
this.log('[OK] All tokens valid, no refresh needed');