mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-14 00:16:38 +00:00
feat(cliproxy): add pinned model routing prefixes
This commit is contained in:
1 parent
850f1ef95c
commit
4d5e52828b
17 files changed
+791
-24
No files matched your search
@@ -0,0 +1,39 @@
|
||||
import {
|
||||
buildCliproxyRoutingHints,
|
||||
type CliproxyProviderRoutingHints,
|
||||
} from '../shared/cliproxy-model-routing';
|
||||
import { fetchCliproxyModels } from './stats-fetcher';
|
||||
import { ensureManagedModelPrefixes } from './managed-model-prefixes';
|
||||
import {
|
||||
getResolvedCatalogSnapshot,
|
||||
type CatalogSource,
|
||||
type ResolvedCatalogSnapshot,
|
||||
} from './catalog-cache';
|
||||
import type { ProviderCatalog } from './model-catalog';
|
||||
import type { CLIProxyProvider } from './types';
|
||||
|
||||
export interface CatalogRoutingSnapshot {
|
||||
catalogs: Partial<Record<CLIProxyProvider, ProviderCatalog>>;
|
||||
source: CatalogSource;
|
||||
cacheAge: string | null;
|
||||
routing: Partial<Record<string, CliproxyProviderRoutingHints>>;
|
||||
}
|
||||
|
||||
export async function getCatalogRoutingSnapshot(): Promise<CatalogRoutingSnapshot> {
|
||||
try {
|
||||
await ensureManagedModelPrefixes();
|
||||
} catch {
|
||||
// Keep catalog rendering non-fatal when prefix sync is unavailable.
|
||||
}
|
||||
|
||||
const snapshot: ResolvedCatalogSnapshot = await getResolvedCatalogSnapshot();
|
||||
const modelsResponse = await fetchCliproxyModels();
|
||||
const routing = buildCliproxyRoutingHints(snapshot.catalogs, modelsResponse?.models ?? []);
|
||||
|
||||
return {
|
||||
catalogs: snapshot.catalogs,
|
||||
source: snapshot.source,
|
||||
cacheAge: snapshot.cacheAge,
|
||||
routing,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
import { getManagedModelPrefix } from '../shared/cliproxy-model-routing';
|
||||
import { buildManagementHeaders, buildProxyUrl, getProxyTarget } from './proxy-target-resolver';
|
||||
import { mapExternalProviderName } from './provider-capabilities';
|
||||
import type { CLIProxyProvider } from './types';
|
||||
|
||||
interface ManagementAuthFileRecord {
|
||||
account_type?: string;
|
||||
name: string;
|
||||
provider?: string;
|
||||
type?: string;
|
||||
}
|
||||
|
||||
export interface ManagedPrefixSyncResult {
|
||||
checked: number;
|
||||
updated: number;
|
||||
}
|
||||
|
||||
function normalizeProvider(record: ManagementAuthFileRecord): CLIProxyProvider | null {
|
||||
const providerName = record.provider?.trim() || record.type?.trim() || '';
|
||||
return providerName ? mapExternalProviderName(providerName) : null;
|
||||
}
|
||||
|
||||
async function listAuthFiles(): Promise<ManagementAuthFileRecord[]> {
|
||||
const target = getProxyTarget();
|
||||
const response = await fetch(buildProxyUrl(target, '/v0/management/auth-files'), {
|
||||
headers: buildManagementHeaders(target),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`auth file listing failed with status ${response.status}`);
|
||||
}
|
||||
|
||||
const data = (await response.json()) as { files?: ManagementAuthFileRecord[] };
|
||||
return Array.isArray(data.files) ? data.files : [];
|
||||
}
|
||||
|
||||
async function patchAuthFilePrefix(name: string, prefix: string): Promise<void> {
|
||||
const target = getProxyTarget();
|
||||
const response = await fetch(buildProxyUrl(target, '/v0/management/auth-files/fields'), {
|
||||
method: 'PATCH',
|
||||
headers: buildManagementHeaders(target, {
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: JSON.stringify({ name, prefix }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`auth file prefix patch failed for ${name} with status ${response.status}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function readAuthFilePrefix(name: string): Promise<string | null> {
|
||||
const target = getProxyTarget();
|
||||
const url = buildProxyUrl(
|
||||
target,
|
||||
`/v0/management/auth-files/download?name=${encodeURIComponent(name)}`
|
||||
);
|
||||
const response = await fetch(url, {
|
||||
headers: buildManagementHeaders(target),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`auth file download failed for ${name} with status ${response.status}`);
|
||||
}
|
||||
|
||||
const content = await response.text();
|
||||
try {
|
||||
const parsed = JSON.parse(content) as { prefix?: unknown };
|
||||
return typeof parsed.prefix === 'string' ? parsed.prefix.trim() : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function ensureManagedModelPrefixes(
|
||||
providers?: CLIProxyProvider[]
|
||||
): Promise<ManagedPrefixSyncResult> {
|
||||
const files = await listAuthFiles();
|
||||
const allowedProviders = new Set((providers ?? []).map((provider) => provider.trim()));
|
||||
let checked = 0;
|
||||
let updated = 0;
|
||||
|
||||
for (const record of files) {
|
||||
if ((record.account_type || '').trim().toLowerCase() !== 'oauth') {
|
||||
continue;
|
||||
}
|
||||
|
||||
const provider = normalizeProvider(record);
|
||||
if (!provider) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (allowedProviders.size > 0 && !allowedProviders.has(provider)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const prefix = getManagedModelPrefix(provider);
|
||||
if (!prefix) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
checked += 1;
|
||||
const currentPrefix = await readAuthFilePrefix(record.name);
|
||||
if (currentPrefix === prefix) {
|
||||
continue;
|
||||
}
|
||||
await patchAuthFilePrefix(record.name, prefix);
|
||||
updated += 1;
|
||||
} catch {
|
||||
// Best-effort repair: skip files that cannot be read or patched.
|
||||
}
|
||||
}
|
||||
|
||||
return { checked, updated };
|
||||
}
|
||||
@@ -6,9 +6,11 @@ import {
|
||||
getResolvedCatalog,
|
||||
refreshCatalogFromProxy,
|
||||
} from '../../cliproxy/catalog-cache';
|
||||
import { getCatalogRoutingSnapshot } from '../../cliproxy/catalog-routing';
|
||||
import { getProxyTarget } from '../../cliproxy/proxy-target-resolver';
|
||||
import type { CLIProxyProvider } from '../../cliproxy/types';
|
||||
import type { RemoteModelInfo } from '../../cliproxy/management-api-types';
|
||||
import type { CliproxyProviderRoutingHints } from '../../shared/cliproxy-model-routing';
|
||||
|
||||
/** Fetch model definitions from CLIProxyAPI for all syncable providers */
|
||||
async function fetchRemoteCatalogs(
|
||||
@@ -44,7 +46,8 @@ export async function handleCatalogStatus(verbose: boolean): Promise<void> {
|
||||
console.log(header('Model Catalog'));
|
||||
console.log('');
|
||||
|
||||
const cacheAge = getCacheAge();
|
||||
const routingSnapshot = await getCatalogRoutingSnapshot();
|
||||
const cacheAge = routingSnapshot.cacheAge ?? getCacheAge();
|
||||
if (cacheAge) {
|
||||
console.log(` Cache: ${color('synced', 'success')} (${cacheAge})`);
|
||||
} else {
|
||||
@@ -55,14 +58,14 @@ export async function handleCatalogStatus(verbose: boolean): Promise<void> {
|
||||
console.log(subheader('Providers:'));
|
||||
|
||||
for (const provider of SYNCABLE_PROVIDERS) {
|
||||
const catalog = getResolvedCatalog(provider);
|
||||
const catalog = routingSnapshot.catalogs[provider] ?? getResolvedCatalog(provider);
|
||||
if (catalog) {
|
||||
const count = catalog.models.length;
|
||||
console.log(` ${color(catalog.displayName.padEnd(20), 'command')} ${count} models`);
|
||||
const routing = routingSnapshot.routing[provider];
|
||||
const suffix = renderRoutingSummary(routing);
|
||||
console.log(` ${color(catalog.displayName.padEnd(20), 'command')} ${count} models${suffix}`);
|
||||
if (verbose) {
|
||||
for (const model of catalog.models) {
|
||||
console.log(dim(` - ${model.id} (${model.name})`));
|
||||
}
|
||||
renderVerboseRouting(provider, catalog.models, routing);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -74,6 +77,60 @@ export async function handleCatalogStatus(verbose: boolean): Promise<void> {
|
||||
console.log('');
|
||||
}
|
||||
|
||||
function renderRoutingSummary(routing: CliproxyProviderRoutingHints | undefined): string {
|
||||
if (!routing) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const parts = [`prefix ${routing.prefix}`];
|
||||
if (routing.shadowedCount > 0) {
|
||||
parts.push(`${routing.shadowedCount} shadowed`);
|
||||
}
|
||||
if (routing.prefixOnlyCount > 0) {
|
||||
parts.push(`${routing.prefixOnlyCount} prefix-only`);
|
||||
}
|
||||
return parts.length > 0 ? ` ${dim(`(${parts.join(', ')})`)}` : '';
|
||||
}
|
||||
|
||||
function renderVerboseRouting(
|
||||
provider: CLIProxyProvider,
|
||||
models: Array<{ id: string; name: string }>,
|
||||
routing: CliproxyProviderRoutingHints | undefined
|
||||
): void {
|
||||
if (!routing) {
|
||||
for (const model of models) {
|
||||
console.log(dim(` - ${model.id} (${model.name})`));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const routingMap = new Map(routing.models.map((hint) => [hint.modelId, hint]));
|
||||
for (const model of models) {
|
||||
const hint = routingMap.get(model.id);
|
||||
console.log(dim(` - ${model.id} (${model.name})`));
|
||||
if (!hint) {
|
||||
continue;
|
||||
}
|
||||
|
||||
console.log(dim(` preferred: ${hint.recommendedModelId}`));
|
||||
if (hint.unprefixedStatus === 'safe') {
|
||||
console.log(dim(` unprefixed: resolves to ${routing.displayName}`));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (hint.unprefixedStatus === 'shadowed' && hint.effectiveDisplayName) {
|
||||
console.log(dim(` unprefixed: currently resolves to ${hint.effectiveDisplayName}`));
|
||||
continue;
|
||||
}
|
||||
|
||||
console.log(dim(` unprefixed: not advertised, use ${hint.recommendedModelId}`));
|
||||
}
|
||||
|
||||
if (provider === 'gemini' || provider === 'agy') {
|
||||
console.log(dim(` short prefix stays backend-pinned even when unprefixed names overlap.`));
|
||||
}
|
||||
}
|
||||
|
||||
/** Refresh catalog from CLIProxyAPI */
|
||||
export async function handleCatalogRefresh(verbose: boolean): Promise<void> {
|
||||
await initUI();
|
||||
|
||||
@@ -11,6 +11,8 @@ import { getProviderAccounts } from '../../cliproxy/account-manager';
|
||||
import { triggerOAuth } from '../../cliproxy/auth/oauth-handler';
|
||||
import { CLIProxyProfileName, CLIPROXY_PROFILES } from '../../auth/profile-detector';
|
||||
import { supportsModelConfig, getProviderCatalog, ModelEntry } from '../../cliproxy/model-catalog';
|
||||
import { ensureManagedModelPrefixes } from '../../cliproxy/managed-model-prefixes';
|
||||
import { getManagedModelPrefix } from '../../shared/cliproxy-model-routing';
|
||||
import { CLIProxyProvider, CLIProxyBackend } from '../../cliproxy/types';
|
||||
import type { TargetType } from '../../targets/target-adapter';
|
||||
import { getPersistedTargetChoices, isPersistedTargetType } from '../../targets/target-metadata';
|
||||
@@ -115,6 +117,11 @@ function formatModelOption(model: ModelEntry): string {
|
||||
return `${model.name}${tierBadge}`;
|
||||
}
|
||||
|
||||
function getSelectableModelId(provider: CLIProxyProvider, modelId: string): string {
|
||||
const prefix = getManagedModelPrefix(provider);
|
||||
return prefix ? `${prefix}/${modelId}` : modelId;
|
||||
}
|
||||
|
||||
function getBackendLabel(backend: CLIProxyBackend): string {
|
||||
return backend === 'plus' ? 'CLIProxy Plus' : 'CLIProxy';
|
||||
}
|
||||
@@ -174,9 +181,17 @@ async function selectTierConfig(
|
||||
// Select model
|
||||
let model: string | undefined;
|
||||
if (supportsModelConfig(provider as CLIProxyProvider)) {
|
||||
try {
|
||||
await ensureManagedModelPrefixes([provider as CLIProxyProvider]);
|
||||
} catch {
|
||||
// Keep interactive model selection available even when prefix repair fails.
|
||||
}
|
||||
const catalog = getProviderCatalog(provider as CLIProxyProvider);
|
||||
if (catalog) {
|
||||
const modelOptions = catalog.models.map((m) => ({ id: m.id, label: formatModelOption(m) }));
|
||||
const modelOptions = catalog.models.map((m) => ({
|
||||
id: getSelectableModelId(provider as CLIProxyProvider, m.id),
|
||||
label: formatModelOption(m),
|
||||
}));
|
||||
const defaultIdx = catalog.models.findIndex((m) => m.id === catalog.defaultModel);
|
||||
model = await InteractivePrompt.selectFromList(`Model for ${tierName}:`, modelOptions, {
|
||||
defaultIndex: defaultIdx >= 0 ? defaultIdx : 0,
|
||||
@@ -426,9 +441,17 @@ export async function handleCreate(
|
||||
let model = parsedArgs.model;
|
||||
if (!model) {
|
||||
if (supportsModelConfig(provider as CLIProxyProvider)) {
|
||||
try {
|
||||
await ensureManagedModelPrefixes([provider as CLIProxyProvider]);
|
||||
} catch {
|
||||
// Keep variant creation available even when prefix repair fails.
|
||||
}
|
||||
const catalog = getProviderCatalog(provider as CLIProxyProvider);
|
||||
if (catalog) {
|
||||
const modelOptions = catalog.models.map((m) => ({ id: m.id, label: formatModelOption(m) }));
|
||||
const modelOptions = catalog.models.map((m) => ({
|
||||
id: getSelectableModelId(provider as CLIProxyProvider, m.id),
|
||||
label: formatModelOption(m),
|
||||
}));
|
||||
const defaultIdx = catalog.models.findIndex((m) => m.id === catalog.defaultModel);
|
||||
model = await InteractivePrompt.selectFromList('Select model:', modelOptions, {
|
||||
defaultIndex: defaultIdx >= 0 ? defaultIdx : 0,
|
||||
@@ -667,10 +690,15 @@ export async function handleEdit(
|
||||
if (changeModel) {
|
||||
const providerForModel = newProvider || (variant.provider as CLIProxyProfileName);
|
||||
if (supportsModelConfig(providerForModel as CLIProxyProvider)) {
|
||||
try {
|
||||
await ensureManagedModelPrefixes([providerForModel as CLIProxyProvider]);
|
||||
} catch {
|
||||
// Keep edit flow available even when prefix repair fails.
|
||||
}
|
||||
const catalog = getProviderCatalog(providerForModel as CLIProxyProvider);
|
||||
if (catalog) {
|
||||
const modelOptions = catalog.models.map((m) => ({
|
||||
id: m.id,
|
||||
id: getSelectableModelId(providerForModel as CLIProxyProvider, m.id),
|
||||
label: formatModelOption(m),
|
||||
}));
|
||||
const defaultIdx = catalog.models.findIndex((m) => m.id === catalog.defaultModel);
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
export const MANAGED_MODEL_PREFIXES = {
|
||||
gemini: 'gcli',
|
||||
agy: 'agy',
|
||||
} as const;
|
||||
|
||||
export type ManagedModelPrefixProvider = keyof typeof MANAGED_MODEL_PREFIXES;
|
||||
export type ModelRoutingStatus = 'safe' | 'shadowed' | 'prefix-only';
|
||||
|
||||
export interface CatalogLikeModel {
|
||||
id: string;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
export interface CatalogLikeProvider {
|
||||
provider: string;
|
||||
displayName: string;
|
||||
models: CatalogLikeModel[];
|
||||
}
|
||||
|
||||
export interface MergedModelLike {
|
||||
id: string;
|
||||
owned_by?: string;
|
||||
type?: string;
|
||||
}
|
||||
|
||||
export interface CliproxyModelRoutingHint {
|
||||
modelId: string;
|
||||
modelName: string;
|
||||
prefix: string;
|
||||
pinnedModelId: string;
|
||||
recommendedModelId: string;
|
||||
unprefixedStatus: ModelRoutingStatus;
|
||||
effectiveProvider: string | null;
|
||||
effectiveDisplayName: string | null;
|
||||
effectiveOwnedBy: string | null;
|
||||
summary: string;
|
||||
}
|
||||
|
||||
export interface CliproxyProviderRoutingHints {
|
||||
provider: string;
|
||||
displayName: string;
|
||||
prefix: string;
|
||||
safeCount: number;
|
||||
shadowedCount: number;
|
||||
prefixOnlyCount: number;
|
||||
models: CliproxyModelRoutingHint[];
|
||||
}
|
||||
|
||||
const PROVIDER_OWNER_HINTS: Record<string, string[]> = {
|
||||
gemini: ['google'],
|
||||
agy: ['antigravity'],
|
||||
claude: ['anthropic'],
|
||||
codex: ['openai'],
|
||||
qwen: ['alibaba', 'qwen'],
|
||||
iflow: ['iflow'],
|
||||
kimi: ['kimi', 'moonshot'],
|
||||
kiro: ['kiro', 'aws'],
|
||||
ghcp: ['github', 'copilot'],
|
||||
};
|
||||
|
||||
function normalize(value: string | null | undefined): string {
|
||||
return typeof value === 'string' ? value.trim().toLowerCase() : '';
|
||||
}
|
||||
|
||||
function getManagedPrefix(provider: string): string | null {
|
||||
const normalizedProvider = normalize(provider);
|
||||
if (normalizedProvider in MANAGED_MODEL_PREFIXES) {
|
||||
return MANAGED_MODEL_PREFIXES[normalizedProvider as ManagedModelPrefixProvider];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function getDisplayName(
|
||||
provider: string,
|
||||
catalogs: Partial<Record<string, CatalogLikeProvider>>
|
||||
): string | null {
|
||||
const normalizedProvider = normalize(provider);
|
||||
const catalog = catalogs[normalizedProvider];
|
||||
return catalog?.displayName?.trim() || null;
|
||||
}
|
||||
|
||||
function inferProvider(model: MergedModelLike): string | null {
|
||||
const type = normalize(model.type);
|
||||
const owner = normalize(model.owned_by);
|
||||
|
||||
const directMatches: Record<string, string> = {
|
||||
antigravity: 'agy',
|
||||
'github-copilot': 'ghcp',
|
||||
copilot: 'ghcp',
|
||||
anthropic: 'claude',
|
||||
'gemini-cli': 'gemini',
|
||||
};
|
||||
|
||||
if (type && directMatches[type]) {
|
||||
return directMatches[type];
|
||||
}
|
||||
|
||||
for (const [provider, hints] of Object.entries(PROVIDER_OWNER_HINTS)) {
|
||||
if (hints.some((hint) => type.includes(hint) || owner.includes(hint))) {
|
||||
return provider;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function buildSummary(
|
||||
providerDisplayName: string,
|
||||
hint: Pick<
|
||||
CliproxyModelRoutingHint,
|
||||
'modelId' | 'pinnedModelId' | 'unprefixedStatus' | 'effectiveDisplayName'
|
||||
>
|
||||
): string {
|
||||
if (hint.unprefixedStatus === 'safe') {
|
||||
return `${hint.modelId} currently resolves to ${providerDisplayName}. Use ${hint.pinnedModelId} to keep it pinned.`;
|
||||
}
|
||||
|
||||
if (hint.unprefixedStatus === 'shadowed' && hint.effectiveDisplayName) {
|
||||
return `${hint.modelId} currently resolves to ${hint.effectiveDisplayName}. Use ${hint.pinnedModelId} to force ${providerDisplayName}.`;
|
||||
}
|
||||
|
||||
return `${hint.modelId} is not advertised unprefixed right now. Use ${hint.pinnedModelId} to target ${providerDisplayName}.`;
|
||||
}
|
||||
|
||||
export function buildCliproxyRoutingHints(
|
||||
catalogs: Partial<Record<string, CatalogLikeProvider>>,
|
||||
mergedModels: MergedModelLike[]
|
||||
): Partial<Record<string, CliproxyProviderRoutingHints>> {
|
||||
const mergedModelMap = new Map<string, MergedModelLike>();
|
||||
for (const model of mergedModels) {
|
||||
const key = normalize(model.id);
|
||||
if (key && !mergedModelMap.has(key)) {
|
||||
mergedModelMap.set(key, model);
|
||||
}
|
||||
}
|
||||
|
||||
const result: Partial<Record<string, CliproxyProviderRoutingHints>> = {};
|
||||
|
||||
for (const [providerKey, catalog] of Object.entries(catalogs)) {
|
||||
if (!catalog) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const prefix = getManagedPrefix(providerKey);
|
||||
if (!prefix) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let safeCount = 0;
|
||||
let shadowedCount = 0;
|
||||
let prefixOnlyCount = 0;
|
||||
|
||||
const models = catalog.models.map((model) => {
|
||||
const mergedModel = mergedModelMap.get(normalize(model.id));
|
||||
const effectiveProvider = mergedModel ? inferProvider(mergedModel) : null;
|
||||
const effectiveDisplayName =
|
||||
effectiveProvider && getDisplayName(effectiveProvider, catalogs)
|
||||
? getDisplayName(effectiveProvider, catalogs)
|
||||
: mergedModel?.owned_by?.trim() || null;
|
||||
|
||||
let unprefixedStatus: ModelRoutingStatus = 'prefix-only';
|
||||
if (!mergedModel) {
|
||||
prefixOnlyCount += 1;
|
||||
} else if (effectiveProvider === providerKey) {
|
||||
unprefixedStatus = 'safe';
|
||||
safeCount += 1;
|
||||
} else {
|
||||
unprefixedStatus = 'shadowed';
|
||||
shadowedCount += 1;
|
||||
}
|
||||
|
||||
const hint: CliproxyModelRoutingHint = {
|
||||
modelId: model.id,
|
||||
modelName: model.name?.trim() || model.id,
|
||||
prefix,
|
||||
pinnedModelId: `${prefix}/${model.id}`,
|
||||
recommendedModelId: `${prefix}/${model.id}`,
|
||||
unprefixedStatus,
|
||||
effectiveProvider,
|
||||
effectiveDisplayName,
|
||||
effectiveOwnedBy: mergedModel?.owned_by?.trim() || null,
|
||||
summary: '',
|
||||
};
|
||||
|
||||
hint.summary = buildSummary(catalog.displayName, hint);
|
||||
return hint;
|
||||
});
|
||||
|
||||
result[providerKey] = {
|
||||
provider: providerKey,
|
||||
displayName: catalog.displayName,
|
||||
prefix,
|
||||
safeCount,
|
||||
shadowedCount,
|
||||
prefixOnlyCount,
|
||||
models,
|
||||
};
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function getManagedModelPrefix(provider: string): string | null {
|
||||
return getManagedPrefix(provider);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Router, Request, Response } from 'express';
|
||||
import { getResolvedCatalogSnapshot } from '../../cliproxy/catalog-cache';
|
||||
import { getCatalogRoutingSnapshot } from '../../cliproxy/catalog-routing';
|
||||
|
||||
const router = Router();
|
||||
|
||||
@@ -9,9 +9,10 @@ const router = Router();
|
||||
*/
|
||||
router.get('/', async (_req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const snapshot = await getResolvedCatalogSnapshot();
|
||||
const snapshot = await getCatalogRoutingSnapshot();
|
||||
res.json({
|
||||
catalogs: snapshot.catalogs,
|
||||
routing: snapshot.routing,
|
||||
source: snapshot.source,
|
||||
cache: {
|
||||
synced: snapshot.source !== 'static' || snapshot.cacheAge !== null,
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
buildManagementHeaders,
|
||||
} from '../../cliproxy/proxy-target-resolver';
|
||||
import { fetchRemoteAuthStatus } from '../../cliproxy/remote-auth-fetcher';
|
||||
import { ensureManagedModelPrefixes } from '../../cliproxy/managed-model-prefixes';
|
||||
import { loadOrCreateUnifiedConfig } from '../../config/unified-config-loader';
|
||||
import { tryKiroImport } from '../../cliproxy/auth/kiro-import';
|
||||
import {
|
||||
@@ -315,6 +316,12 @@ export function getStartAuthNicknameError(
|
||||
*/
|
||||
router.get('/', async (_req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
try {
|
||||
await ensureManagedModelPrefixes();
|
||||
} catch {
|
||||
// Keep auth status available even when prefix repair cannot run.
|
||||
}
|
||||
|
||||
// Check if remote mode is enabled
|
||||
const target = getProxyTarget();
|
||||
if (target.isRemote) {
|
||||
@@ -386,6 +393,12 @@ router.get('/', async (_req: Request, res: Response): Promise<void> => {
|
||||
*/
|
||||
router.get('/accounts', async (_req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
try {
|
||||
await ensureManagedModelPrefixes();
|
||||
} catch {
|
||||
// Non-fatal: account listing should still work without prefix repair.
|
||||
}
|
||||
|
||||
// Check if remote mode is enabled
|
||||
const target = getProxyTarget();
|
||||
if (target.isRemote) {
|
||||
@@ -677,6 +690,12 @@ router.post('/:provider/start', async (req: Request, res: Response): Promise<voi
|
||||
});
|
||||
|
||||
if (account) {
|
||||
try {
|
||||
await ensureManagedModelPrefixes([account.provider]);
|
||||
} catch {
|
||||
// Keep OAuth success path non-fatal when prefix repair cannot run.
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
account: {
|
||||
@@ -1011,6 +1030,11 @@ router.get('/:provider/status', async (req: Request, res: Response): Promise<voi
|
||||
}
|
||||
|
||||
pendingManualAuthState.delete(state);
|
||||
try {
|
||||
await ensureManagedModelPrefixes([account.provider]);
|
||||
} catch {
|
||||
// Keep manual callback success path non-fatal when prefix repair cannot run.
|
||||
}
|
||||
res.json({
|
||||
status: 'ok',
|
||||
account: {
|
||||
|
||||
Reference in new issue
Block a user