mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-12 22:14:22 +00:00
fix(cliproxy): harden pinned routing follow-up
This commit is contained in:
1 parent
b6aa6fb0d4
commit
40c4816acf
13 files changed
+124
-50
No files matched your search
@@ -20,7 +20,7 @@ export interface CatalogRoutingSnapshot {
|
|||||||
|
|
||||||
export async function getCatalogRoutingSnapshot(): Promise<CatalogRoutingSnapshot> {
|
export async function getCatalogRoutingSnapshot(): Promise<CatalogRoutingSnapshot> {
|
||||||
const snapshot: ResolvedCatalogSnapshot = await getResolvedCatalogSnapshot();
|
const snapshot: ResolvedCatalogSnapshot = await getResolvedCatalogSnapshot();
|
||||||
const modelsResponse = await fetchCliproxyModels();
|
const modelsResponse = snapshot.source === 'live' ? await fetchCliproxyModels() : null;
|
||||||
const routing = buildCliproxyRoutingHints(snapshot.catalogs, modelsResponse?.models ?? []);
|
const routing = buildCliproxyRoutingHints(snapshot.catalogs, modelsResponse?.models ?? []);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import { buildManagementHeaders, buildProxyUrl, getProxyTarget } from './proxy-t
|
|||||||
import { mapExternalProviderName } from './provider-capabilities';
|
import { mapExternalProviderName } from './provider-capabilities';
|
||||||
import type { CLIProxyProvider } from './types';
|
import type { CLIProxyProvider } from './types';
|
||||||
|
|
||||||
|
const MANAGED_PREFIX_REQUEST_TIMEOUT_MS = 3000;
|
||||||
|
|
||||||
interface ManagementAuthFileRecord {
|
interface ManagementAuthFileRecord {
|
||||||
account_type?: string;
|
account_type?: string;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -25,11 +27,24 @@ function normalizeProvider(record: ManagementAuthFileRecord): CLIProxyProvider |
|
|||||||
return providerName ? mapExternalProviderName(providerName) : null;
|
return providerName ? mapExternalProviderName(providerName) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function listAuthFiles(): Promise<ManagementAuthFileRecord[]> {
|
async function fetchManagementEndpoint(path: string, init: RequestInit = {}): Promise<Response> {
|
||||||
const target = getProxyTarget();
|
const target = getProxyTarget();
|
||||||
const response = await fetch(buildProxyUrl(target, '/v0/management/auth-files'), {
|
const controller = new AbortController();
|
||||||
headers: buildManagementHeaders(target),
|
const timeoutId = setTimeout(() => controller.abort(), MANAGED_PREFIX_REQUEST_TIMEOUT_MS);
|
||||||
});
|
|
||||||
|
try {
|
||||||
|
return await fetch(buildProxyUrl(target, path), {
|
||||||
|
...init,
|
||||||
|
headers: buildManagementHeaders(target, init.headers as Record<string, string> | undefined),
|
||||||
|
signal: controller.signal,
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function listAuthFiles(): Promise<ManagementAuthFileRecord[]> {
|
||||||
|
const response = await fetchManagementEndpoint('/v0/management/auth-files');
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`auth file listing failed with status ${response.status}`);
|
throw new Error(`auth file listing failed with status ${response.status}`);
|
||||||
@@ -40,12 +55,11 @@ async function listAuthFiles(): Promise<ManagementAuthFileRecord[]> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function patchAuthFilePrefix(name: string, prefix: string): Promise<void> {
|
async function patchAuthFilePrefix(name: string, prefix: string): Promise<void> {
|
||||||
const target = getProxyTarget();
|
const response = await fetchManagementEndpoint('/v0/management/auth-files/fields', {
|
||||||
const response = await fetch(buildProxyUrl(target, '/v0/management/auth-files/fields'), {
|
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
headers: buildManagementHeaders(target, {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
}),
|
},
|
||||||
body: JSON.stringify({ name, prefix }),
|
body: JSON.stringify({ name, prefix }),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -55,14 +69,9 @@ async function patchAuthFilePrefix(name: string, prefix: string): Promise<void>
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function readAuthFileMetadata(name: string): Promise<AuthFileMetadata> {
|
async function readAuthFileMetadata(name: string): Promise<AuthFileMetadata> {
|
||||||
const target = getProxyTarget();
|
const response = await fetchManagementEndpoint(
|
||||||
const url = buildProxyUrl(
|
|
||||||
target,
|
|
||||||
`/v0/management/auth-files/download?name=${encodeURIComponent(name)}`
|
`/v0/management/auth-files/download?name=${encodeURIComponent(name)}`
|
||||||
);
|
);
|
||||||
const response = await fetch(url, {
|
|
||||||
headers: buildManagementHeaders(target),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`auth file download failed for ${name} with status ${response.status}`);
|
throw new Error(`auth file download failed for ${name} with status ${response.status}`);
|
||||||
@@ -89,8 +98,17 @@ async function readAuthFileMetadata(name: string): Promise<AuthFileMetadata> {
|
|||||||
export async function ensureManagedModelPrefixes(
|
export async function ensureManagedModelPrefixes(
|
||||||
providers?: CLIProxyProvider[]
|
providers?: CLIProxyProvider[]
|
||||||
): Promise<ManagedPrefixSyncResult> {
|
): Promise<ManagedPrefixSyncResult> {
|
||||||
|
const allowedProviders = new Set(
|
||||||
|
(providers ?? [])
|
||||||
|
.map((provider) => provider.trim())
|
||||||
|
.filter((provider) => getManagedModelPrefix(provider))
|
||||||
|
);
|
||||||
|
|
||||||
|
if (providers && allowedProviders.size === 0) {
|
||||||
|
return { checked: 0, updated: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
const files = await listAuthFiles();
|
const files = await listAuthFiles();
|
||||||
const allowedProviders = new Set((providers ?? []).map((provider) => provider.trim()));
|
|
||||||
let checked = 0;
|
let checked = 0;
|
||||||
let updated = 0;
|
let updated = 0;
|
||||||
|
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ function getSelectableModelId(
|
|||||||
const hint = routing?.models.find(
|
const hint = routing?.models.find(
|
||||||
(entry) => entry.modelId.toLowerCase() === modelId.toLowerCase()
|
(entry) => entry.modelId.toLowerCase() === modelId.toLowerCase()
|
||||||
);
|
);
|
||||||
return hint?.pinnedAvailable ? hint.recommendedModelId : modelId;
|
return hint?.recommendedModelId ?? modelId;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getBackendLabel(backend: CLIProxyBackend): string {
|
function getBackendLabel(backend: CLIProxyBackend): string {
|
||||||
|
|||||||
@@ -162,9 +162,7 @@ export function buildCliproxyRoutingHints(
|
|||||||
.map((candidate) => candidate.id)
|
.map((candidate) => candidate.id)
|
||||||
.sort((left, right) => left.localeCompare(right));
|
.sort((left, right) => left.localeCompare(right));
|
||||||
const managedPinnedId = `${prefix}/${model.id}`;
|
const managedPinnedId = `${prefix}/${model.id}`;
|
||||||
const recommendedModelId = pinnedCandidates.includes(managedPinnedId)
|
const pinnedAvailable = pinnedCandidates.includes(managedPinnedId);
|
||||||
? managedPinnedId
|
|
||||||
: (pinnedCandidates[0] ?? managedPinnedId);
|
|
||||||
const mergedModel = mergedModelMap.get(normalize(model.id));
|
const mergedModel = mergedModelMap.get(normalize(model.id));
|
||||||
const effectiveProvider = mergedModel ? inferProvider(mergedModel) : null;
|
const effectiveProvider = mergedModel ? inferProvider(mergedModel) : null;
|
||||||
const effectiveDisplayName =
|
const effectiveDisplayName =
|
||||||
@@ -188,8 +186,8 @@ export function buildCliproxyRoutingHints(
|
|||||||
modelName: model.name?.trim() || model.id,
|
modelName: model.name?.trim() || model.id,
|
||||||
prefix,
|
prefix,
|
||||||
pinnedModelId: managedPinnedId,
|
pinnedModelId: managedPinnedId,
|
||||||
recommendedModelId,
|
recommendedModelId: managedPinnedId,
|
||||||
pinnedAvailable: pinnedCandidates.length > 0,
|
pinnedAvailable,
|
||||||
unprefixedStatus,
|
unprefixedStatus,
|
||||||
effectiveProvider,
|
effectiveProvider,
|
||||||
effectiveDisplayName,
|
effectiveDisplayName,
|
||||||
|
|||||||
@@ -1017,7 +1017,6 @@ router.get('/:provider/status', async (req: Request, res: Response): Promise<voi
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
pendingManualAuthState.delete(state);
|
|
||||||
try {
|
try {
|
||||||
await ensureManagedModelPrefixes([account.provider]);
|
await ensureManagedModelPrefixes([account.provider]);
|
||||||
} catch {
|
} catch {
|
||||||
@@ -1033,6 +1032,7 @@ router.get('/:provider/status', async (req: Request, res: Response): Promise<voi
|
|||||||
isDefault: account.isDefault,
|
isDefault: account.isDefault,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
pendingManualAuthState.delete(state);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1167,7 +1167,11 @@ router.post('/:provider/submit-callback', async (req: Request, res: Response): P
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (parsed.state) {
|
if (parsed.state) {
|
||||||
pendingManualAuthState.delete(parsed.state);
|
try {
|
||||||
|
await ensureManagedModelPrefixes([account.provider]);
|
||||||
|
} catch {
|
||||||
|
// Keep manual callback success path non-fatal when prefix repair cannot run.
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
@@ -1180,6 +1184,9 @@ router.post('/:provider/submit-callback', async (req: Request, res: Response): P
|
|||||||
isDefault: account.isDefault,
|
isDefault: account.isDefault,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
if (parsed.state) {
|
||||||
|
pendingManualAuthState.delete(parsed.state);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -111,6 +111,16 @@ describe('ensureManagedModelPrefixes', () => {
|
|||||||
expect(downloadedNames).toEqual(['gemini-main']);
|
expect(downloadedNames).toEqual(['gemini-main']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('returns immediately when called for providers without managed prefixes', async () => {
|
||||||
|
const fetchMock = mock(() => Promise.reject(new Error('should not fetch')));
|
||||||
|
global.fetch = fetchMock as typeof fetch;
|
||||||
|
|
||||||
|
const result = await ensureManagedModelPrefixes(['codex']);
|
||||||
|
|
||||||
|
expect(result).toEqual({ checked: 0, updated: 0 });
|
||||||
|
expect(fetchMock).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
it('skips files that already have the managed prefix or a different custom prefix', async () => {
|
it('skips files that already have the managed prefix or a different custom prefix', async () => {
|
||||||
const requests = installFetchMock({
|
const requests = installFetchMock({
|
||||||
files: [
|
files: [
|
||||||
|
|||||||
@@ -64,4 +64,24 @@ describe('cliproxy model routing hints', () => {
|
|||||||
effectiveProvider: null,
|
effectiveProvider: null,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('does not promote custom auth-file prefixes as managed pinned model ids', () => {
|
||||||
|
const routing = buildCliproxyRoutingHints(
|
||||||
|
{
|
||||||
|
gemini: {
|
||||||
|
provider: 'gemini',
|
||||||
|
displayName: 'Gemini',
|
||||||
|
models: [{ id: 'gemini-3-flash-preview', name: 'Gemini Flash' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[{ id: 'team-a/gemini-3-flash-preview', owned_by: 'google', type: 'gemini-cli' }]
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(routing.gemini?.models[0]).toMatchObject({
|
||||||
|
pinnedModelId: 'gcli/gemini-3-flash-preview',
|
||||||
|
recommendedModelId: 'gcli/gemini-3-flash-preview',
|
||||||
|
pinnedAvailable: false,
|
||||||
|
unprefixedStatus: 'prefix-only',
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
@@ -28,7 +28,7 @@ function normalizePresetValues(
|
|||||||
const hint = routing?.models.find(
|
const hint = routing?.models.find(
|
||||||
(entry) => entry.modelId.toLowerCase() === modelId.toLowerCase()
|
(entry) => entry.modelId.toLowerCase() === modelId.toLowerCase()
|
||||||
);
|
);
|
||||||
return hint?.pinnedAvailable ? hint.recommendedModelId : modelId;
|
return hint?.recommendedModelId ?? modelId;
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -60,9 +60,7 @@ export function ModelConfigSection({
|
|||||||
[routing]
|
[routing]
|
||||||
);
|
);
|
||||||
const toPreferredModelId = (modelId: string): string =>
|
const toPreferredModelId = (modelId: string): string =>
|
||||||
routingHintMap.get(modelId.toLowerCase())?.pinnedAvailable
|
routingHintMap.get(modelId.toLowerCase())?.recommendedModelId ?? modelId;
|
||||||
? (routingHintMap.get(modelId.toLowerCase())?.recommendedModelId ?? modelId)
|
|
||||||
: modelId;
|
|
||||||
|
|
||||||
const extendedContextModels = useMemo(() => {
|
const extendedContextModels = useMemo(() => {
|
||||||
if (!catalog) return [];
|
if (!catalog) return [];
|
||||||
|
|||||||
@@ -310,10 +310,7 @@ function getPreferredOptionValue(
|
|||||||
modelId: string,
|
modelId: string,
|
||||||
routingHint: CliproxyProviderRoutingHints['models'][number] | undefined
|
routingHint: CliproxyProviderRoutingHints['models'][number] | undefined
|
||||||
): string {
|
): string {
|
||||||
if (!routingHint?.pinnedAvailable) {
|
return routingHint?.recommendedModelId ?? modelId;
|
||||||
return modelId;
|
|
||||||
}
|
|
||||||
return routingHint.recommendedModelId;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function FlexibleModelSelector({
|
export function FlexibleModelSelector({
|
||||||
@@ -338,6 +335,15 @@ export function FlexibleModelSelector({
|
|||||||
new Map((routing?.models ?? []).map((hint) => [hint.modelId.toLowerCase(), hint] as const)),
|
new Map((routing?.models ?? []).map((hint) => [hint.modelId.toLowerCase(), hint] as const)),
|
||||||
[routing]
|
[routing]
|
||||||
);
|
);
|
||||||
|
const recommendedOptionValues = useMemo(
|
||||||
|
() =>
|
||||||
|
new Set(
|
||||||
|
resolvedCatalogModels.map((model) =>
|
||||||
|
getPreferredOptionValue(model.id, routingHints.get(model.id.toLowerCase()))
|
||||||
|
)
|
||||||
|
),
|
||||||
|
[resolvedCatalogModels, routingHints]
|
||||||
|
);
|
||||||
const selectedRoutingHint = useMemo(
|
const selectedRoutingHint = useMemo(
|
||||||
() => routingHints.get(normalizeModelValue(value, routing).toLowerCase()),
|
() => routingHints.get(normalizeModelValue(value, routing).toLowerCase()),
|
||||||
[routing, routingHints, value]
|
[routing, routingHints, value]
|
||||||
@@ -384,6 +390,12 @@ export function FlexibleModelSelector({
|
|||||||
|
|
||||||
const allModelOptions = allModels
|
const allModelOptions = allModels
|
||||||
.filter((model) => !catalogModelIds.has(model.id))
|
.filter((model) => !catalogModelIds.has(model.id))
|
||||||
|
.filter(
|
||||||
|
(model) =>
|
||||||
|
!recommendedOptionValues.has(
|
||||||
|
getPreferredOptionValue(model.id, routingHints.get(model.id.toLowerCase()))
|
||||||
|
)
|
||||||
|
)
|
||||||
.map((model) => ({
|
.map((model) => ({
|
||||||
value: getPreferredOptionValue(model.id, routingHints.get(model.id.toLowerCase())),
|
value: getPreferredOptionValue(model.id, routingHints.get(model.id.toLowerCase())),
|
||||||
groupKey: 'all',
|
groupKey: 'all',
|
||||||
|
|||||||
@@ -47,6 +47,11 @@ const MAX_POLL_DURATION = 5 * 60 * 1000;
|
|||||||
/** Fail visibly after repeated poll transport errors instead of retrying forever */
|
/** Fail visibly after repeated poll transport errors instead of retrying forever */
|
||||||
const MAX_POLL_FAILURES = 3;
|
const MAX_POLL_FAILURES = 3;
|
||||||
|
|
||||||
|
function invalidateCliproxyRoutingData(queryClient: ReturnType<typeof useQueryClient>): void {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['cliproxy-catalog'] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['cliproxy-models'] });
|
||||||
|
}
|
||||||
|
|
||||||
async function parseResponseBody(response: Response): Promise<Record<string, unknown>> {
|
async function parseResponseBody(response: Response): Promise<Record<string, unknown>> {
|
||||||
const text = await response.text();
|
const text = await response.text();
|
||||||
if (!text) return {};
|
if (!text) return {};
|
||||||
@@ -167,6 +172,7 @@ export function useCliproxyAuthFlow() {
|
|||||||
queryClient.invalidateQueries({ queryKey: ['cliproxy-auth'] });
|
queryClient.invalidateQueries({ queryKey: ['cliproxy-auth'] });
|
||||||
queryClient.invalidateQueries({ queryKey: ['cliproxy-accounts'] });
|
queryClient.invalidateQueries({ queryKey: ['cliproxy-accounts'] });
|
||||||
queryClient.invalidateQueries({ queryKey: ['account-quota'] });
|
queryClient.invalidateQueries({ queryKey: ['account-quota'] });
|
||||||
|
invalidateCliproxyRoutingData(queryClient);
|
||||||
toast.success(`${provider} authentication successful`);
|
toast.success(`${provider} authentication successful`);
|
||||||
openedAuthUrlRef.current = false;
|
openedAuthUrlRef.current = false;
|
||||||
setState(INITIAL_STATE);
|
setState(INITIAL_STATE);
|
||||||
@@ -301,6 +307,7 @@ export function useCliproxyAuthFlow() {
|
|||||||
queryClient.invalidateQueries({ queryKey: ['cliproxy-auth'] });
|
queryClient.invalidateQueries({ queryKey: ['cliproxy-auth'] });
|
||||||
queryClient.invalidateQueries({ queryKey: ['cliproxy-accounts'] });
|
queryClient.invalidateQueries({ queryKey: ['cliproxy-accounts'] });
|
||||||
queryClient.invalidateQueries({ queryKey: ['account-quota'] });
|
queryClient.invalidateQueries({ queryKey: ['account-quota'] });
|
||||||
|
invalidateCliproxyRoutingData(queryClient);
|
||||||
// Note: No toast here - DeviceCodeDialog's useDeviceCode hook handles success toast
|
// Note: No toast here - DeviceCodeDialog's useDeviceCode hook handles success toast
|
||||||
// via deviceCodeCompleted WebSocket event to avoid duplicate toasts
|
// via deviceCodeCompleted WebSocket event to avoid duplicate toasts
|
||||||
openedAuthUrlRef.current = false;
|
openedAuthUrlRef.current = false;
|
||||||
@@ -467,6 +474,7 @@ export function useCliproxyAuthFlow() {
|
|||||||
queryClient.invalidateQueries({ queryKey: ['cliproxy-auth'] });
|
queryClient.invalidateQueries({ queryKey: ['cliproxy-auth'] });
|
||||||
queryClient.invalidateQueries({ queryKey: ['cliproxy-accounts'] });
|
queryClient.invalidateQueries({ queryKey: ['cliproxy-accounts'] });
|
||||||
queryClient.invalidateQueries({ queryKey: ['account-quota'] });
|
queryClient.invalidateQueries({ queryKey: ['account-quota'] });
|
||||||
|
invalidateCliproxyRoutingData(queryClient);
|
||||||
toast.success(`${currentProvider} authentication successful`);
|
toast.success(`${currentProvider} authentication successful`);
|
||||||
setState(INITIAL_STATE);
|
setState(INITIAL_STATE);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -14,6 +14,17 @@ import {
|
|||||||
} from '@/lib/api-client';
|
} from '@/lib/api-client';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
function invalidateCliproxyRoutingQueries(queryClient: ReturnType<typeof useQueryClient>): void {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['cliproxy-catalog'] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['cliproxy-models'] });
|
||||||
|
}
|
||||||
|
|
||||||
|
function invalidateCliproxyAccountQueries(queryClient: ReturnType<typeof useQueryClient>): void {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['cliproxy-accounts'] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['cliproxy-auth'] });
|
||||||
|
invalidateCliproxyRoutingQueries(queryClient);
|
||||||
|
}
|
||||||
|
|
||||||
export function useCliproxy() {
|
export function useCliproxy() {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ['cliproxy'],
|
queryKey: ['cliproxy'],
|
||||||
@@ -128,8 +139,7 @@ export function useSetDefaultAccount() {
|
|||||||
mutationFn: ({ provider, accountId }: { provider: string; accountId: string }) =>
|
mutationFn: ({ provider, accountId }: { provider: string; accountId: string }) =>
|
||||||
api.cliproxy.accounts.setDefault(provider, accountId),
|
api.cliproxy.accounts.setDefault(provider, accountId),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['cliproxy-accounts'] });
|
invalidateCliproxyAccountQueries(queryClient);
|
||||||
queryClient.invalidateQueries({ queryKey: ['cliproxy-auth'] });
|
|
||||||
toast.success('Default account updated');
|
toast.success('Default account updated');
|
||||||
},
|
},
|
||||||
onError: (error: Error) => {
|
onError: (error: Error) => {
|
||||||
@@ -145,8 +155,7 @@ export function useRemoveAccount() {
|
|||||||
mutationFn: ({ provider, accountId }: { provider: string; accountId: string }) =>
|
mutationFn: ({ provider, accountId }: { provider: string; accountId: string }) =>
|
||||||
api.cliproxy.accounts.remove(provider, accountId),
|
api.cliproxy.accounts.remove(provider, accountId),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['cliproxy-accounts'] });
|
invalidateCliproxyAccountQueries(queryClient);
|
||||||
queryClient.invalidateQueries({ queryKey: ['cliproxy-auth'] });
|
|
||||||
toast.success('Account removed');
|
toast.success('Account removed');
|
||||||
},
|
},
|
||||||
onError: (error: Error) => {
|
onError: (error: Error) => {
|
||||||
@@ -162,8 +171,7 @@ export function usePauseAccount() {
|
|||||||
mutationFn: ({ provider, accountId }: { provider: string; accountId: string }) =>
|
mutationFn: ({ provider, accountId }: { provider: string; accountId: string }) =>
|
||||||
api.cliproxy.accounts.pause(provider, accountId),
|
api.cliproxy.accounts.pause(provider, accountId),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['cliproxy-accounts'] });
|
invalidateCliproxyAccountQueries(queryClient);
|
||||||
queryClient.invalidateQueries({ queryKey: ['cliproxy-auth'] });
|
|
||||||
queryClient.invalidateQueries({ queryKey: ['cliproxy-stats'] });
|
queryClient.invalidateQueries({ queryKey: ['cliproxy-stats'] });
|
||||||
toast.success('Account paused');
|
toast.success('Account paused');
|
||||||
},
|
},
|
||||||
@@ -180,8 +188,7 @@ export function useResumeAccount() {
|
|||||||
mutationFn: ({ provider, accountId }: { provider: string; accountId: string }) =>
|
mutationFn: ({ provider, accountId }: { provider: string; accountId: string }) =>
|
||||||
api.cliproxy.accounts.resume(provider, accountId),
|
api.cliproxy.accounts.resume(provider, accountId),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['cliproxy-accounts'] });
|
invalidateCliproxyAccountQueries(queryClient);
|
||||||
queryClient.invalidateQueries({ queryKey: ['cliproxy-auth'] });
|
|
||||||
queryClient.invalidateQueries({ queryKey: ['cliproxy-stats'] });
|
queryClient.invalidateQueries({ queryKey: ['cliproxy-stats'] });
|
||||||
toast.success('Account resumed');
|
toast.success('Account resumed');
|
||||||
},
|
},
|
||||||
@@ -198,8 +205,7 @@ export function useSoloAccount() {
|
|||||||
mutationFn: ({ provider, accountId }: { provider: string; accountId: string }) =>
|
mutationFn: ({ provider, accountId }: { provider: string; accountId: string }) =>
|
||||||
api.cliproxy.accounts.solo(provider, accountId),
|
api.cliproxy.accounts.solo(provider, accountId),
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['cliproxy-accounts'] });
|
invalidateCliproxyAccountQueries(queryClient);
|
||||||
queryClient.invalidateQueries({ queryKey: ['cliproxy-auth'] });
|
|
||||||
queryClient.invalidateQueries({ queryKey: ['cliproxy-stats'] });
|
queryClient.invalidateQueries({ queryKey: ['cliproxy-stats'] });
|
||||||
const pausedCount = data.paused.length;
|
const pausedCount = data.paused.length;
|
||||||
toast.success(
|
toast.success(
|
||||||
@@ -219,8 +225,7 @@ export function useBulkPauseAccounts() {
|
|||||||
mutationFn: ({ provider, accountIds }: { provider: string; accountIds: string[] }) =>
|
mutationFn: ({ provider, accountIds }: { provider: string; accountIds: string[] }) =>
|
||||||
api.cliproxy.accounts.bulkPause(provider, accountIds),
|
api.cliproxy.accounts.bulkPause(provider, accountIds),
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['cliproxy-accounts'] });
|
invalidateCliproxyAccountQueries(queryClient);
|
||||||
queryClient.invalidateQueries({ queryKey: ['cliproxy-auth'] });
|
|
||||||
queryClient.invalidateQueries({ queryKey: ['cliproxy-stats'] });
|
queryClient.invalidateQueries({ queryKey: ['cliproxy-stats'] });
|
||||||
toast.success(
|
toast.success(
|
||||||
`Paused ${data.succeeded.length} account${data.succeeded.length !== 1 ? 's' : ''}`
|
`Paused ${data.succeeded.length} account${data.succeeded.length !== 1 ? 's' : ''}`
|
||||||
@@ -244,8 +249,7 @@ export function useBulkResumeAccounts() {
|
|||||||
mutationFn: ({ provider, accountIds }: { provider: string; accountIds: string[] }) =>
|
mutationFn: ({ provider, accountIds }: { provider: string; accountIds: string[] }) =>
|
||||||
api.cliproxy.accounts.bulkResume(provider, accountIds),
|
api.cliproxy.accounts.bulkResume(provider, accountIds),
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['cliproxy-accounts'] });
|
invalidateCliproxyAccountQueries(queryClient);
|
||||||
queryClient.invalidateQueries({ queryKey: ['cliproxy-auth'] });
|
|
||||||
queryClient.invalidateQueries({ queryKey: ['cliproxy-stats'] });
|
queryClient.invalidateQueries({ queryKey: ['cliproxy-stats'] });
|
||||||
toast.success(
|
toast.success(
|
||||||
`Resumed ${data.succeeded.length} account${data.succeeded.length !== 1 ? 's' : ''}`
|
`Resumed ${data.succeeded.length} account${data.succeeded.length !== 1 ? 's' : ''}`
|
||||||
@@ -270,8 +274,7 @@ export function useStartAuth() {
|
|||||||
mutationFn: ({ provider, nickname }: { provider: string; nickname?: string }) =>
|
mutationFn: ({ provider, nickname }: { provider: string; nickname?: string }) =>
|
||||||
api.cliproxy.auth.start(provider, nickname),
|
api.cliproxy.auth.start(provider, nickname),
|
||||||
onSuccess: (_data, variables) => {
|
onSuccess: (_data, variables) => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['cliproxy-accounts'] });
|
invalidateCliproxyAccountQueries(queryClient);
|
||||||
queryClient.invalidateQueries({ queryKey: ['cliproxy-auth'] });
|
|
||||||
toast.success(`Account added for ${variables.provider}`);
|
toast.success(`Account added for ${variables.provider}`);
|
||||||
},
|
},
|
||||||
onError: (error: Error) => {
|
onError: (error: Error) => {
|
||||||
@@ -297,8 +300,7 @@ export function useKiroImport() {
|
|||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: () => api.cliproxy.auth.kiroImport(),
|
mutationFn: () => api.cliproxy.auth.kiroImport(),
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['cliproxy-accounts'] });
|
invalidateCliproxyAccountQueries(queryClient);
|
||||||
queryClient.invalidateQueries({ queryKey: ['cliproxy-auth'] });
|
|
||||||
if (data.account) {
|
if (data.account) {
|
||||||
toast.success(`Imported Kiro account: ${data.account.email || data.account.id}`);
|
toast.success(`Imported Kiro account: ${data.account.email || data.account.id}`);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -306,6 +306,7 @@ export function CliproxyPage() {
|
|||||||
queryClient.invalidateQueries({ queryKey: ['cliproxy'] });
|
queryClient.invalidateQueries({ queryKey: ['cliproxy'] });
|
||||||
queryClient.invalidateQueries({ queryKey: ['cliproxy-auth'] });
|
queryClient.invalidateQueries({ queryKey: ['cliproxy-auth'] });
|
||||||
queryClient.invalidateQueries({ queryKey: ['cliproxy-catalog'] });
|
queryClient.invalidateQueries({ queryKey: ['cliproxy-catalog'] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['cliproxy-models'] });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePauseToggle = (provider: string, accountId: string, paused: boolean) => {
|
const handlePauseToggle = (provider: string, accountId: string, paused: boolean) => {
|
||||||
|
|||||||
Reference in new issue
Block a user