Skip to content

Commit 24576b0

Browse files
fix(sync): clear stale providers and cap retries when CLIProxy unavailable on startup
After a PC restart, the CLIProxy may be down or not yet have API keys loaded, causing OpenAI GPT models to return "Unauthorized: Invalid API key" because opencode.json retained stale ccs-* entries with the fake ccs-internal-managed token. - Cap discoverProviderModels retries at 5 (default) via maxAttempts option, preventing infinite retry loops when CLIProxy is unreachable on startup (ECONNREFUSED) - On startup sync failure, clear all ccs-* providers from opencode.json so broken entries don't persist; watch mode continues so the next .settings.json change triggers a fresh sync once CCS is available
1 parent d0e5643 commit 24576b0

2 files changed

Lines changed: 55 additions & 1 deletion

File tree

src/cliproxy/client.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export interface DiscoverProviderModelsOptions {
2222
fetchFn?: FetchLike;
2323
sleep?: (delayMs: number) => Promise<void>;
2424
abortSignal?: AbortSignal;
25+
maxAttempts?: number;
2526
}
2627

2728
function asNonEmptyString(value: unknown): string | undefined {
@@ -110,6 +111,7 @@ export async function discoverProviderModels(
110111
const sleep: (delayMs: number) => Promise<void> = options.sleep ?? defaultSleep;
111112
const endpoint: string = buildModelsEndpoint(options.runtimeBaseUrl, options.provider);
112113

114+
const maxAttempts = options.maxAttempts ?? 5;
113115
let attempt = 0;
114116
while (true) {
115117
try {
@@ -136,6 +138,10 @@ export async function discoverProviderModels(
136138
throw error;
137139
}
138140

141+
if (attempt >= maxAttempts - 1) {
142+
throw error;
143+
}
144+
139145
await sleep(nextBackoffDelayMs(attempt));
140146
attempt += 1;
141147
}

src/watch/watch.ts

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { watch } from 'chokidar';
22
import { createHash } from 'node:crypto';
33
import { dirname, normalize } from 'node:path';
44
import { readFile } from 'node:fs/promises';
5+
import { resolveCcsConfigPath, resolveOpenCodeConfigPath } from '../config/paths.js';
6+
import { applyManagedConfigSync, hasEffectiveChanges } from '../opencode/patch.js';
57
import type { RunSyncOptions, SyncResult } from '../sync/service.js';
68
import { runSync } from '../sync/service.js';
79

@@ -95,9 +97,55 @@ export function shouldTriggerWatchSync(
9597
return fileName.endsWith('.settings.json');
9698
}
9799

100+
async function clearManagedProviders(options: RunWatchModeOptions, resolvedPaths: { opencodeConfigPath: string; ccsConfigPath: string }): Promise<void> {
101+
try {
102+
const currentConfig = await options.readFile(resolvedPaths.opencodeConfigPath);
103+
const cleared = applyManagedConfigSync(currentConfig, { providers: {}, defaultModel: '' });
104+
if (hasEffectiveChanges(currentConfig, cleared)) {
105+
await options.writeFile(resolvedPaths.opencodeConfigPath, cleared);
106+
}
107+
} catch {
108+
// If config is unreadable/unwritable, there's nothing to clear
109+
}
110+
}
111+
98112
export async function runWatchMode(options: RunWatchModeOptions): Promise<SyncResult> {
99113
const guard = createWriteLoopGuard({ suppressionWindowMs: 1500 });
100-
let lastResult: SyncResult = await runSync({ ...options, watch: true });
114+
115+
let lastResult: SyncResult;
116+
try {
117+
lastResult = await runSync({ ...options, watch: true });
118+
} catch (startupError) {
119+
// CCS/CLIProxy unavailable on startup — resolve paths independently and clear stale
120+
// ccs-* providers from opencode.json so broken entries don't persist across restarts.
121+
const opencodeConfigPath = resolveOpenCodeConfigPath({
122+
explicitPath: options.opencodeConfigPath,
123+
cwd: options.cwd,
124+
homeDir: options.homeDir,
125+
exists: options.pathExists,
126+
});
127+
const ccsConfigPath = resolveCcsConfigPath({
128+
explicitPath: options.ccsConfigPath,
129+
cwd: options.cwd,
130+
homeDir: options.homeDir,
131+
});
132+
const resolvedPaths = { opencodeConfigPath, ccsConfigPath };
133+
134+
if (!options.dryRun) {
135+
await clearManagedProviders(options, resolvedPaths);
136+
}
137+
138+
// Synthesize a failed result so watch mode can still set up file watching.
139+
// When CCS becomes available and writes a .settings.json, the next sync will succeed.
140+
lastResult = {
141+
ok: false,
142+
mode: 'watch',
143+
changed: false,
144+
resolvedPaths,
145+
providers: [],
146+
summary: 'CCS startup sync failed — waiting for CCS to become available.',
147+
};
148+
}
101149

102150
if (lastResult.changed && !options.dryRun) {
103151
const initialHash = await safeReadHash(lastResult.resolvedPaths.opencodeConfigPath);

0 commit comments

Comments
 (0)