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 authored and GitHub committed 2026-02-07 03:34:14 -05:00
1 parent 8172c65d1e
commit 215c00e983
4 files changed
+55 -10

No files matched your search

+33 -1
View File
@@ -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,
+8 -3
View File
@@ -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
+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. * 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 };
} }
+5 -2
View File
@@ -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');