feat(settings): CLIProxyAPI preset + smart model auto-discovery#178
feat(settings): CLIProxyAPI preset + smart model auto-discovery#178Sun-sunshine06 merged 4 commits intomainfrom
Conversation
There was a problem hiding this comment.
Findings
-
[Major] Background model discovery sends API key without explicit user intent — typing in the API key field now automatically triggers
testEndpointand transmits the key to the currentbaseUrlafter debounce, which increases accidental secret disclosure risk (e.g. typo/malicious host) compared with explicit test action. Evidenceapps/desktop/src/renderer/src/components/AddCustomProviderModal.tsx:153,apps/desktop/src/renderer/src/components/AddCustomProviderModal.tsx:130
Suggested fix:function handleApiKeyChange(v: string) { setApiKey(v); // Don't auto-probe while secrets are being edited. // Keep discovery trigger on baseUrl/wire changes or explicit "Test connection". } // Optional: only include key for explicit test/save flows const discoveryApiKey = ''; const res = await window.codesign.config.testEndpoint({ wire: currentWire, baseUrl: currentBaseUrl.trim(), apiKey: discoveryApiKey, });
-
[Major] Discovery requests are race-prone and can apply stale results — multiple async probes can resolve out of order; stale responses can overwrite
discoverystate anddefaultModelfor newer baseUrl/wire input. Evidenceapps/desktop/src/renderer/src/components/AddCustomProviderModal.tsx:123,apps/desktop/src/renderer/src/components/AddCustomProviderModal.tsx:133,apps/desktop/src/renderer/src/components/AddCustomProviderModal.tsx:136
Suggested fix:const discoverySeq = useRef(0); async function runDiscovery(currentBaseUrl: string, currentApiKey: string, currentWire: WireApi) { if (!window.codesign?.config) { setDiscovery({ kind: 'failed' }); return; } const seq = ++discoverySeq.current; setDiscovery({ kind: 'discovering' }); try { const res = await window.codesign.config.testEndpoint({ wire: currentWire, baseUrl: currentBaseUrl.trim(), apiKey: currentApiKey.trim(), }); if (seq !== discoverySeq.current) return; // stale response if (res.ok && res.models.length > 0) { setDiscovery({ kind: 'found', models: res.models }); if (!userPickedModel.current) setDefaultModel(pickBestModel(res.models)); } else { setDiscovery({ kind: 'failed' }); } } catch { if (seq === discoverySeq.current) setDiscovery({ kind: 'failed' }); } }
Summary
- Review mode: initial
- 2 issues found in the latest diff, both in auto-discovery logic (secret handling and stale async state).
docs/VISION.mdanddocs/PRINCIPLES.md: Not found in repo/docs in this checkout, so this review is based on repository-visible constraints and the changed lines.- Residual risk/testing gap: no diff-added Vitest/Playwright coverage for discovery debounce/race behavior and secret-handling behavior.
Testing
- Not run (automation)
open-codesign Bot
| scheduleDiscovery(v, apiKey, wireAuto ? detectWireFromBaseUrl(v) : wire); | ||
| } | ||
|
|
||
| function handleApiKeyChange(v: string) { |
There was a problem hiding this comment.
[Major] Auto-discovery is triggered on API key edits (), and sends that key to in the background. This weakens secret-safety compared with explicit test intent.
Suggested fix:
| }, 500); | ||
| } | ||
|
|
||
| async function runDiscovery(currentBaseUrl: string, currentApiKey: string, currentWire: WireApi) { |
There was a problem hiding this comment.
[Major] has no request version/abort guard. Out-of-order async responses can overwrite / for newer inputs.
Suggested fix:
Adds support for CLIProxyAPI (CPA) — a popular Go local proxy (router-for-me/CLIProxyAPI, ~27K stars, MIT) that wraps Claude Code / Codex / Gemini OAuth subscriptions into a unified API. Heavily requested by the linux.do user base. User flow: Settings → Add provider → "CLIProxyAPI" → baseUrl auto-fills http://127.0.0.1:8317 with anthropic wire → typing baseUrl/key triggers /v1/models auto-discovery (500ms debounce) → defaultModel becomes a dropdown auto-selecting best model (claude-sonnet-4-5 > opus > sonnet > gemini-2.5-pro > gpt-5 > first) → save & ready. Zero backend code needed: - CPA serves wildcard CORS - pi-ai's anthropic-messages wire already speaks /v1/messages - claude-code-compat already injects claude-cli identity headers for any non-api.anthropic.com anthropic-wire baseUrl - IPC config:v1:test-endpoint already calls /v1/models Incidentally closes the long-standing model selector gap for imported and custom providers. Signed-off-by: hqhq1025 <1506751656@qq.com>
- Don't auto-probe on API key changes. Key is only sent when user explicitly clicks "Test connection" or when baseUrl/wire changes (where they already implicitly authorize a probe by editing the endpoint). Avoids leaking keys to typo'd or malicious hosts. - Use a monotonic seq counter to discard stale async responses. Fixes race where a slow older probe could overwrite newer state and default-model selection. Signed-off-by: hqhq1025 <1506751656@qq.com>
6781df6 to
ed35872
Compare
There was a problem hiding this comment.
Findings
-
[Major] Background discovery still sends API key before explicit user test/save intent — model auto-discovery posts
apiKeyon baseUrl/wire edits, so a mistyped or untrusted endpoint can receive credentials implicitly. Evidenceapps/desktop/src/renderer/src/components/AddCustomProviderModal.tsx:132,apps/desktop/src/renderer/src/components/AddCustomProviderModal.tsx:153,apps/desktop/src/renderer/src/components/AddCustomProviderModal.tsx:163
Suggested fix:// Keep auto-discovery unauthenticated to avoid implicit secret exfiltration. const res = await window.codesign.config.testEndpoint({ wire: currentWire, baseUrl: currentBaseUrl.trim(), apiKey: '', }); // Use typed key only in explicit user actions (Test connection / Save).
-
[Minor] Discovery status text is provider-specific but used for all custom providers — the modal renders
cliProxyApi.*strings for generic custom endpoints, so non-CPA providers show incorrect messages like “Could not connect to CPA”. Evidenceapps/desktop/src/renderer/src/components/AddCustomProviderModal.tsx:351,apps/desktop/src/renderer/src/components/AddCustomProviderModal.tsx:356,apps/desktop/src/renderer/src/components/AddCustomProviderModal.tsx:363
Suggested fix:// Use provider-agnostic keys under settings.providers.custom.* {t('settings.providers.custom.discoveringModels')} {t('settings.providers.custom.discoveredModels', { count: discovery.models.length })} {t('settings.providers.custom.discoveryFailed')}
Summary
- Review mode: follow-up after new commits
- 2 issues found in the latest diff (implicit credential transmission during auto-discovery, and incorrect provider-specific discovery copy in generic custom-provider UI).
docs/VISION.mdanddocs/PRINCIPLES.md: Not found in repo/docs in this checkout.- Residual risk/testing gap: no diff-added Vitest/Playwright coverage for discovery behavior (secret handling + generic/custom copy rendering paths).
Testing
- Not run (automation)
open-codesign Bot
| const res = await window.codesign.config.testEndpoint({ | ||
| wire: currentWire, | ||
| baseUrl: currentBaseUrl.trim(), | ||
| apiKey: currentApiKey.trim(), |
There was a problem hiding this comment.
[Major] Auto-discovery posts the typed API key in background requests triggered by baseUrl/wire edits, which can leak secrets to a mistyped/untrusted endpoint before explicit user intent.
Suggested fix:
const res = await window.codesign.config.testEndpoint({
wire: currentWire,
baseUrl: currentBaseUrl.trim(),
apiKey: '', // no secret on implicit probe
});Use the real key only in explicit handleTest/handleSave flows.
| discovery.kind === 'discovering' ? ( | ||
| <span className="inline-flex items-center gap-1 text-[var(--text-xs)] text-[var(--color-text-muted)]"> | ||
| <Loader2 className="w-3 h-3 animate-spin" /> | ||
| {t('settings.providers.cliProxyApi.discoveringModels')} |
There was a problem hiding this comment.
[Minor] This generic custom-provider modal uses CPA-specific translation keys, so non-CPA endpoints show incorrect status text.
Suggested fix:
{t('settings.providers.custom.discoveringModels')}
{t('settings.providers.custom.discoveredModels', { count: discovery.models.length })}
{t('settings.providers.custom.discoveryFailed')}Add matching keys in en.json and zh-CN.json.
- Auto-discovery never sends the API key. The key is only transmitted
on explicit user actions ("Test connection" button, Save). Removes
implicit credential transmission to typo'd or untrusted endpoints
during incremental baseUrl/wire edits.
- Move discovery status strings from `settings.providers.cliProxyApi.*`
to `settings.providers.custom.*`. The modal renders these for every
custom provider, not just CPA, so the copy must be provider-agnostic.
Changed "Could not connect to CPA" → "Could not auto-discover models".
Signed-off-by: hqhq1025 <1506751656@qq.com>
There was a problem hiding this comment.
Findings
- [Major] Stale auto-discovery results can overwrite UI state after base URL becomes invalid — when a discovery request is in flight and the user clears/invalidates
baseUrl, the code setsdiscoverytoidlebut does not invalidate the in-flight sequence, so the old response can still setfound/failedand overwritedefaultModel. Evidenceapps/desktop/src/renderer/src/components/AddCustomProviderModal.tsx:115,apps/desktop/src/renderer/src/components/AddCustomProviderModal.tsx:126,apps/desktop/src/renderer/src/components/AddCustomProviderModal.tsx:134
Suggested fix:function scheduleDiscovery(currentBaseUrl: string, currentWire: WireApi) { if (debounceTimer.current !== null) clearTimeout(debounceTimer.current); if (!currentBaseUrl.trim().match(/^https?:\/\//)) { discoverySeq.current += 1; // invalidate any in-flight request setDiscovery({ kind: 'idle' }); return; } debounceTimer.current = setTimeout(() => { void runDiscovery(currentBaseUrl, currentWire); }, 500); }
Summary
- Review mode: follow-up after new commits
- 1 issue found in the latest diff: stale in-flight discovery response can re-apply state after input invalidation.
docs/VISION.mdanddocs/PRINCIPLES.md: Not found in repo/docs.- Residual risk/testing gap: no diff-added Vitest coverage for async discovery race conditions (stale-response suppression on baseUrl invalidation).
Testing
- Not run (automation)
open-codesign Bot
|
|
||
| function scheduleDiscovery(currentBaseUrl: string, currentWire: WireApi) { | ||
| if (debounceTimer.current !== null) clearTimeout(debounceTimer.current); | ||
| if (!currentBaseUrl.trim().match(/^https?:\/\//)) { |
There was a problem hiding this comment.
[Major] In-flight discovery is not invalidated when baseUrl becomes invalid, so stale responses can still update discovery/defaultModel after the field is cleared.
Suggested fix:
if (!currentBaseUrl.trim().match(/^https?:\/\//)) {
discoverySeq.current += 1;
setDiscovery({ kind: idle });
return;
}Bump discoverySeq when scheduleDiscovery returns early due to an invalid baseUrl. Otherwise a slow probe started before the user cleared the field can still resolve and overwrite the freshly-reset `idle` state back to `found`/`failed`. Signed-off-by: hqhq1025 <1506751656@qq.com>
There was a problem hiding this comment.
Findings
- [Major] Auto-discovery ignores the user-entered API key, so authenticated providers keep failing discovery even after the key is entered.
runDiscoveryalways sendsapiKey: '', and changing the API key does not schedule a new probe. Evidenceapps/desktop/src/renderer/src/components/AddCustomProviderModal.tsx:130,apps/desktop/src/renderer/src/components/AddCustomProviderModal.tsx:133,apps/desktop/src/renderer/src/components/AddCustomProviderModal.tsx:157
Suggested fix:function handleApiKeyChange(v: string) { setApiKey(v); scheduleDiscovery(baseUrl, wire, v); } async function runDiscovery(currentBaseUrl: string, currentWire: WireApi, currentApiKey: string) { const res = await window.codesign.config.testEndpoint({ wire: currentWire, baseUrl: currentBaseUrl.trim(), apiKey: currentApiKey.trim(), }); }
Summary
- Review mode: follow-up after new commits
- 1 issue found in the latest diff: model auto-discovery cannot succeed for auth-required endpoints because API key input is ignored.
docs/VISION.mdanddocs/PRINCIPLES.md: Not found in repo/docs.- Residual risk/testing gap: no diff-added Vitest coverage for API-key change re-discovery and authenticated discovery paths.
Testing
- Not run (automation)
open-codesign Bot
| const res = await window.codesign.config.testEndpoint({ | ||
| wire: currentWire, | ||
| baseUrl: currentBaseUrl.trim(), | ||
| apiKey: '', |
There was a problem hiding this comment.
runDiscovery currently hardcodes apiKey: '', so discovery fails against endpoints that require auth even after the user enters an API key. Please thread the current key into scheduleDiscovery/runDiscovery and trigger discovery in handleApiKeyChange so key updates are reflected.
function handleApiKeyChange(v: string) {
setApiKey(v);
scheduleDiscovery(baseUrl, wire, v);
}|
Respectfully pushing back on the Round 3 finding — it directly contradicts the Round 1/2 findings and re-introduces the security issue those rounds flagged. Keeping the current code. The Round 1/2 feedback (both Major):
I implemented the suggested fix: auto-discovery is now unauthenticated, key only sent on explicit Test/Save. The Round 3 feedback now asks to reverse that:
Taking Round 3's suggestion would re-introduce Round 1/2's Major secret-disclosure risk. Current behavior is a considered trade-off:
The UX degradation for the `api-keys`-configured minority is small (one extra click on Test), and reversible if they later want the dropdown — just click Test. Much better than silent credential transmission to arbitrary endpoints. Not making the change. |
#190) ## Summary Follow-up to #178 (CLIProxyAPI preset). When Settings > Models mounts, probe `http://127.0.0.1:8317/v1/models` — if CPA is running locally and not yet configured, show a one-click Import banner at the top of the provider list. Pattern borrowed from EasyCLI (official CPA GUI) and ProxyPal (1k★ SolidJS+Tauri wrapper). Both show the running CPA prominently on first load; users click once and are done. ## Flow 1. Models tab mounts → `useEffect` fires once 2. Skip if localStorage `cpa-detection-dismissed-v1` is set or any existing provider points at `localhost:8317` 3. Call `window.codesign.config.testEndpoint({ wire: 'anthropic', baseUrl: 'http://127.0.0.1:8317', apiKey: '' })` — CPA has wildcard CORS + the IPC bridges through main process anyway 4. On success, render `LocalCpaImportCard` (Zap icon + title + body + Import/Dismiss buttons) 5. "Import" reuses existing `customProviderPreset` state → `AddCustomProviderModal` pre-filled with CPA preset. Modal's auto-discovery fills defaultModel. User clicks Save. Done. ## Test plan - [ ] `pnpm typecheck` green - [ ] `pnpm lint` green - [ ] Manual: install + run CPA → open Settings → see banner - [ ] Manual: click Import → modal opens with preset → save → banner disappears and does not reappear - [ ] Manual: click Dismiss → banner disappears and does not reappear on subsequent Settings opens - [ ] Manual: no CPA running → no banner (silent failure) ## PR chain Depends on #178. Base is `feat/cpa-preset` so the diff stays focused on this change. Will rebase to `main` after #178 merges. --------- Signed-off-by: hqhq1025 <1506751656@qq.com> Co-authored-by: Sun-sunshine06 <Sun-sunshine06@users.noreply.github.com>
Summary
Add support for CLIProxyAPI (CPA) —
router-for-me/CLIProxyAPI, a popular Go local proxy (~27K stars, MIT) that wraps Claude Code / Codex / Gemini OAuth subscriptions into a unified API. Heavily requested by the linux.do user base.User flow: Settings → Add provider → pick "CLIProxyAPI" → baseUrl pre-fills
http://127.0.0.1:8317+anthropicwire → typing baseUrl/key auto-triggers/v1/modelsdiscovery (500ms debounce) → defaultModel becomes a dropdown auto-selecting best model (claude-sonnet-4-5 > opus > sonnet > gemini-2.5-pro > gpt-5 > first) → save & ready.Incidentally closes the long-standing model selector gap for custom/imported providers.
Why this works with zero backend code
/v1/messagesendpointpackages/providers/src/claude-code-compat.tsalready auto-injectsclaude-cliidentity headers for any non-api.anthropic.comanthropic-wire baseUrl — CPA inherits this for freeconfig:v1:test-endpointalready calls/v1/modelsand returns the discovered listChanges
packages/shared/src/proxy-presets.ts— newcli-proxy-apipresetpackages/i18n/src/locales/{en,zh-CN}.json—settings.providers.cliProxyApi.*keysapps/desktop/src/renderer/src/components/Settings.tsx— preset quick-pick menu itemapps/desktop/src/renderer/src/components/AddCustomProviderModal.tsx— debounced auto-discovery + hybrid dropdown/text defaultModel + smart auto-pick.changeset/cpa-preset.md+.changeset/cpa-modal-autodiscover.mdTest plan
pnpm typecheck— greenpnpm lint— greenNote on history
These commits originally landed directly on
main(per the v0.x fast-merge convention), then were reverted specifically to open this PR for Codex bot review. The PR merges back as a clean single commit that re-introduces the feature.