pi-cliproxyapi-autoconnect 1.2.0 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -0
- package/index.ts +68 -68
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -289,6 +289,11 @@ CLIProxyAPI 官方建议调大 nginx 的 `proxy_read_timeout`,Cloudflare 免
|
|
|
289
289
|
|
|
290
290
|
## 更新日志
|
|
291
291
|
|
|
292
|
+
### 1.2.1
|
|
293
|
+
|
|
294
|
+
- pi TUI 内的登录提示、地址选择、错误信息和 `/cliproxy` 输出全部改为英文。
|
|
295
|
+
- provider 显示名仍为 `CLIProxy`,`/login` 入口、`/cliproxy` 命令和模型前缀 `cliproxy/` 不变。
|
|
296
|
+
|
|
292
297
|
### 1.2.0
|
|
293
298
|
|
|
294
299
|
- 包名从 `pi-cliproxy` 改为 `pi-cliproxyapi-autoconnect`;provider ID、`cliproxy.json`、`/cliproxy` 命令和模型前缀 `cliproxy/` 保持不变。
|
package/index.ts
CHANGED
|
@@ -51,7 +51,7 @@ import { getAgentDir, type ExtensionAPI } from "@earendil-works/pi-coding-agent"
|
|
|
51
51
|
|
|
52
52
|
const PROVIDER_ID = "cliproxy";
|
|
53
53
|
const PROVIDER_NAME = "CLIProxy";
|
|
54
|
-
const AUTH_NAME = "CLIProxy
|
|
54
|
+
const AUTH_NAME = "CLIProxy (domain + API key)";
|
|
55
55
|
const CONFIG_FILE_NAME = "cliproxy.json";
|
|
56
56
|
const CONFIG_VERSION = 2;
|
|
57
57
|
const REQUEST_TIMEOUT_MS = 10_000;
|
|
@@ -65,9 +65,9 @@ let lastRefreshError: string | undefined;
|
|
|
65
65
|
type SupportedApi = "openai-completions" | "anthropic-messages" | "google-generative-ai";
|
|
66
66
|
|
|
67
67
|
const PROVIDER_APIS: Readonly<Record<SupportedApi, string>> = {
|
|
68
|
-
"openai-completions": "OpenAI
|
|
69
|
-
"anthropic-messages": "Anthropic
|
|
70
|
-
"google-generative-ai": "Gemini
|
|
68
|
+
"openai-completions": "OpenAI compatible",
|
|
69
|
+
"anthropic-messages": "Anthropic compatible",
|
|
70
|
+
"google-generative-ai": "Gemini compatible",
|
|
71
71
|
};
|
|
72
72
|
|
|
73
73
|
// =============================================================================
|
|
@@ -480,7 +480,7 @@ function isLocalHostname(hostname: string): boolean {
|
|
|
480
480
|
*/
|
|
481
481
|
function normalizeDomain(input: string): NormalizedDomain {
|
|
482
482
|
const original = input.trim();
|
|
483
|
-
if (!original) throw new Error("
|
|
483
|
+
if (!original) throw new Error("Domain is required.");
|
|
484
484
|
const implicitScheme = !/^[a-z][a-z\d+.-]*:\/\//iu.test(original);
|
|
485
485
|
const withScheme = implicitScheme
|
|
486
486
|
? `${isLocalHostname(original.split("/")[0]?.split(":")[0] ?? "") ? "http" : "https"}://${original}`
|
|
@@ -489,10 +489,10 @@ function normalizeDomain(input: string): NormalizedDomain {
|
|
|
489
489
|
try {
|
|
490
490
|
parsed = new URL(withScheme);
|
|
491
491
|
} catch {
|
|
492
|
-
throw new Error(
|
|
492
|
+
throw new Error(`Could not parse domain: "${input}". Example: proxy.example.com or https://proxy.example.com`);
|
|
493
493
|
}
|
|
494
494
|
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
495
|
-
throw new Error(
|
|
495
|
+
throw new Error(`Only http/https URLs are supported; received: ${parsed.protocol}`);
|
|
496
496
|
}
|
|
497
497
|
parsed.username = "";
|
|
498
498
|
parsed.password = "";
|
|
@@ -527,7 +527,7 @@ function geminiBaseUrl(rootUrl: string): string {
|
|
|
527
527
|
|
|
528
528
|
function abortSignalWithTimeout(signal: AbortSignal, timeoutMs: number): { signal: AbortSignal; cleanup: () => void } {
|
|
529
529
|
const controller = new AbortController();
|
|
530
|
-
const timer = setTimeout(() => controller.abort(new Error("
|
|
530
|
+
const timer = setTimeout(() => controller.abort(new Error("Request timed out")), timeoutMs);
|
|
531
531
|
const onAbort = () => controller.abort(signal.reason);
|
|
532
532
|
if (signal.aborted) controller.abort(signal.reason);
|
|
533
533
|
else signal.addEventListener("abort", onAbort, { once: true });
|
|
@@ -549,12 +549,12 @@ async function safeText(response: Response): Promise<string> {
|
|
|
549
549
|
}
|
|
550
550
|
|
|
551
551
|
function describeFetchError(error: unknown, ownSignal: AbortSignal, outerSignal: AbortSignal): string {
|
|
552
|
-
if (outerSignal.aborted) return "
|
|
553
|
-
if (ownSignal.aborted) return "
|
|
552
|
+
if (outerSignal.aborted) return "Cancelled";
|
|
553
|
+
if (ownSignal.aborted) return "Request timed out";
|
|
554
554
|
if (error instanceof Error) {
|
|
555
555
|
const cause = error.cause;
|
|
556
556
|
if (cause instanceof Error && cause.message && cause.message !== error.message) {
|
|
557
|
-
return `${error.message}
|
|
557
|
+
return `${error.message} (${cause.message})`;
|
|
558
558
|
}
|
|
559
559
|
return error.message;
|
|
560
560
|
}
|
|
@@ -572,7 +572,7 @@ function failureKindForStatus(status: number): FetchFailure["kind"] {
|
|
|
572
572
|
function makeHttpFailure(status: number, body: string): FetchFailure {
|
|
573
573
|
return {
|
|
574
574
|
ok: false,
|
|
575
|
-
error: `HTTP ${status}${body ?
|
|
575
|
+
error: `HTTP ${status}${body ? `: ${body}` : ""}`,
|
|
576
576
|
kind: failureKindForStatus(status),
|
|
577
577
|
status,
|
|
578
578
|
};
|
|
@@ -587,8 +587,8 @@ async function probeCliproxy(rootUrl: string, signal: AbortSignal): Promise<stri
|
|
|
587
587
|
});
|
|
588
588
|
if (!response.ok) return undefined;
|
|
589
589
|
const body = (await response.json().catch(() => undefined)) as { status?: unknown } | undefined;
|
|
590
|
-
if (body?.status === "ok") return "
|
|
591
|
-
return "
|
|
590
|
+
if (body?.status === "ok") return "Detected CLIProxyAPI (/healthz OK)";
|
|
591
|
+
return "Detected /healthz, but the response is not CLIProxyAPI format";
|
|
592
592
|
} catch {
|
|
593
593
|
return undefined;
|
|
594
594
|
} finally {
|
|
@@ -638,7 +638,7 @@ async function fetchOpenAIModels(baseUrl: string, apiKey: string, signal: AbortS
|
|
|
638
638
|
headers: {
|
|
639
639
|
Accept: "application/json",
|
|
640
640
|
Authorization: `Bearer ${apiKey}`,
|
|
641
|
-
"User-Agent": "pi-cliproxyapi-autoconnect/1.2",
|
|
641
|
+
"User-Agent": "pi-cliproxyapi-autoconnect/1.2.1",
|
|
642
642
|
},
|
|
643
643
|
signal: requestSignal,
|
|
644
644
|
});
|
|
@@ -648,7 +648,7 @@ async function fetchOpenAIModels(baseUrl: string, apiKey: string, signal: AbortS
|
|
|
648
648
|
}
|
|
649
649
|
const payload = await response.json().catch(() => undefined);
|
|
650
650
|
const models = parseOpenAIModelList(payload);
|
|
651
|
-
if (!models) return { ok: false, error: "
|
|
651
|
+
if (!models) return { ok: false, error: "Response is not an OpenAI-compatible model list (missing data array)", kind: "format" };
|
|
652
652
|
return { ok: true, models };
|
|
653
653
|
} catch (error) {
|
|
654
654
|
return { ok: false, error: describeFetchError(error, requestSignal, signal), kind: "network" };
|
|
@@ -662,7 +662,7 @@ async function fetchGeminiModels(baseUrl: string, apiKey: string, signal: AbortS
|
|
|
662
662
|
const { signal: requestSignal, cleanup } = abortSignalWithTimeout(signal, REQUEST_TIMEOUT_MS);
|
|
663
663
|
try {
|
|
664
664
|
const response = await fetch(url, {
|
|
665
|
-
headers: { Accept: "application/json", "User-Agent": "pi-cliproxyapi-autoconnect/1.2" },
|
|
665
|
+
headers: { Accept: "application/json", "User-Agent": "pi-cliproxyapi-autoconnect/1.2.1" },
|
|
666
666
|
signal: requestSignal,
|
|
667
667
|
});
|
|
668
668
|
if (!response.ok) {
|
|
@@ -671,7 +671,7 @@ async function fetchGeminiModels(baseUrl: string, apiKey: string, signal: AbortS
|
|
|
671
671
|
}
|
|
672
672
|
const payload = await response.json().catch(() => undefined);
|
|
673
673
|
const models = parseOpenAIModelList(payload);
|
|
674
|
-
if (!models) return { ok: false, error: "
|
|
674
|
+
if (!models) return { ok: false, error: "Response is not a Gemini-compatible model list (missing models array)", kind: "format" };
|
|
675
675
|
return { ok: true, models };
|
|
676
676
|
} catch (error) {
|
|
677
677
|
return { ok: false, error: describeFetchError(error, requestSignal, signal), kind: "network" };
|
|
@@ -703,7 +703,7 @@ async function fetchAnthropicModels(baseUrl: string, apiKey: string, signal: Abo
|
|
|
703
703
|
// Anthropic's x-api-key header; sending both is harmless upstream.
|
|
704
704
|
Authorization: `Bearer ${apiKey}`,
|
|
705
705
|
"anthropic-version": "2023-06-01",
|
|
706
|
-
"User-Agent": "pi-cliproxyapi-autoconnect/1.2",
|
|
706
|
+
"User-Agent": "pi-cliproxyapi-autoconnect/1.2.1",
|
|
707
707
|
},
|
|
708
708
|
signal: requestSignal,
|
|
709
709
|
});
|
|
@@ -712,7 +712,7 @@ async function fetchAnthropicModels(baseUrl: string, apiKey: string, signal: Abo
|
|
|
712
712
|
if (response.status === 404 || response.status === 405 || response.status === 501) {
|
|
713
713
|
return {
|
|
714
714
|
ok: false,
|
|
715
|
-
error: `Anthropic
|
|
715
|
+
error: `The Anthropic-compatible endpoint does not expose a usable model list (HTTP ${response.status})`,
|
|
716
716
|
kind: "unsupported",
|
|
717
717
|
status: response.status,
|
|
718
718
|
};
|
|
@@ -721,7 +721,7 @@ async function fetchAnthropicModels(baseUrl: string, apiKey: string, signal: Abo
|
|
|
721
721
|
}
|
|
722
722
|
const payload = await response.json().catch(() => undefined);
|
|
723
723
|
const models = parseOpenAIModelList(payload);
|
|
724
|
-
if (!models) return { ok: false, error: "
|
|
724
|
+
if (!models) return { ok: false, error: "Response is not an Anthropic-compatible model list (missing data array)", kind: "format" };
|
|
725
725
|
return { ok: true, models };
|
|
726
726
|
} catch (error) {
|
|
727
727
|
return { ok: false, error: describeFetchError(error, requestSignal, signal), kind: "network" };
|
|
@@ -833,9 +833,9 @@ function isAuthFailure(failure: FetchFailure | undefined): boolean {
|
|
|
833
833
|
|
|
834
834
|
function authFailureMessage(failure: FetchFailure): string {
|
|
835
835
|
if (failure.error.includes("unsafe_example_api_key")) {
|
|
836
|
-
return "
|
|
836
|
+
return "CLIProxyAPI is still using the example API key (unsafe_example_api_key). Update access.api-keys on the server and restart CLIProxyAPI.";
|
|
837
837
|
}
|
|
838
|
-
return `API
|
|
838
|
+
return `API key verification failed: ${failure.error}`;
|
|
839
839
|
}
|
|
840
840
|
|
|
841
841
|
function normalizeApiKey(value: string): string {
|
|
@@ -844,7 +844,7 @@ function normalizeApiKey(value: string): string {
|
|
|
844
844
|
|
|
845
845
|
function compactMessage(value: string, maxLength = 180): string {
|
|
846
846
|
const singleLine = value.replace(/\s+/gu, " ").trim();
|
|
847
|
-
return singleLine.length > maxLength ? `${singleLine.slice(0, maxLength)}
|
|
847
|
+
return singleLine.length > maxLength ? `${singleLine.slice(0, maxLength)}...` : singleLine;
|
|
848
848
|
}
|
|
849
849
|
|
|
850
850
|
/**
|
|
@@ -870,7 +870,7 @@ function summarizeDiscoveryAttempts(attempts: DiscoveryAttempt[]): string {
|
|
|
870
870
|
return attempts
|
|
871
871
|
.map(
|
|
872
872
|
({ candidate, failure }) =>
|
|
873
|
-
`${PROVIDER_APIS[candidate.api]} ${candidate.baseUrl}
|
|
873
|
+
`${PROVIDER_APIS[candidate.api]} ${candidate.baseUrl}: ${compactMessage(failure.error)}`,
|
|
874
874
|
)
|
|
875
875
|
.join("\n");
|
|
876
876
|
}
|
|
@@ -905,10 +905,10 @@ async function autoDiscoverModels(
|
|
|
905
905
|
async function runLogin(interaction: ProviderAuthInteraction): Promise<ApiKeyCredential> {
|
|
906
906
|
const domainInput = await interaction.prompt({
|
|
907
907
|
type: "text",
|
|
908
|
-
message: "CLIProxy
|
|
908
|
+
message: "CLIProxy bound domain (for example proxy.example.com or https://proxy.example.com)",
|
|
909
909
|
placeholder: "proxy.example.com",
|
|
910
910
|
});
|
|
911
|
-
if (!domainInput.trim()) throw new Error("
|
|
911
|
+
if (!domainInput.trim()) throw new Error("Domain is required.");
|
|
912
912
|
|
|
913
913
|
const { rootUrl: initialRootUrl, implicitScheme } = normalizeDomain(domainInput);
|
|
914
914
|
const promptForApiKey = async (message: string): Promise<string> => {
|
|
@@ -919,10 +919,10 @@ async function runLogin(interaction: ProviderAuthInteraction): Promise<ApiKeyCre
|
|
|
919
919
|
placeholder: "sk-...",
|
|
920
920
|
}),
|
|
921
921
|
);
|
|
922
|
-
if (!value) throw new Error("
|
|
922
|
+
if (!value) throw new Error("API key is required.");
|
|
923
923
|
return value;
|
|
924
924
|
};
|
|
925
|
-
let apiKey = await promptForApiKey("CLIProxy API
|
|
925
|
+
let apiKey = await promptForApiKey("CLIProxy API key (stored in pi's auth.json)");
|
|
926
926
|
|
|
927
927
|
// A bare host may be either http or https; only try the alternate scheme
|
|
928
928
|
// automatically when the user did not spell one out.
|
|
@@ -944,7 +944,7 @@ async function runLogin(interaction: ProviderAuthInteraction): Promise<ApiKeyCre
|
|
|
944
944
|
autoDiscoverModels(candidates, apiKey, interaction.signal, (candidate, index, total) => {
|
|
945
945
|
interaction.notify({
|
|
946
946
|
type: "progress",
|
|
947
|
-
message:
|
|
947
|
+
message: `Trying ${PROVIDER_APIS[candidate.api]}: ${candidate.baseUrl} (${index + 1}/${total})...`,
|
|
948
948
|
});
|
|
949
949
|
});
|
|
950
950
|
|
|
@@ -962,9 +962,9 @@ async function runLogin(interaction: ProviderAuthInteraction): Promise<ApiKeyCre
|
|
|
962
962
|
if (authFailure) {
|
|
963
963
|
interaction.notify({
|
|
964
964
|
type: "info",
|
|
965
|
-
message:
|
|
965
|
+
message: `All compatible endpoints failed authentication: ${compactMessage(authFailure.error)}\nEnter the API key again.`,
|
|
966
966
|
});
|
|
967
|
-
apiKey = await promptForApiKey("
|
|
967
|
+
apiKey = await promptForApiKey("Re-enter the CLIProxy API key");
|
|
968
968
|
outcome = await discover();
|
|
969
969
|
authFailure = outcome.ok ? undefined : firstAuthFailure(outcome.attempts);
|
|
970
970
|
if (authFailure) throw new Error(authFailureMessage(authFailure));
|
|
@@ -979,7 +979,7 @@ async function runLogin(interaction: ProviderAuthInteraction): Promise<ApiKeyCre
|
|
|
979
979
|
lastError = undefined;
|
|
980
980
|
interaction.notify({
|
|
981
981
|
type: "info",
|
|
982
|
-
message:
|
|
982
|
+
message: `Selected ${PROVIDER_APIS[api]}: ${baseUrl} (${models.length} models).`,
|
|
983
983
|
});
|
|
984
984
|
} else if (outcome.empty) {
|
|
985
985
|
// Every address answered, but no upstream credentials are available.
|
|
@@ -988,26 +988,26 @@ async function runLogin(interaction: ProviderAuthInteraction): Promise<ApiKeyCre
|
|
|
988
988
|
baseUrl = outcome.empty.baseUrl;
|
|
989
989
|
api = outcome.empty.api;
|
|
990
990
|
models = [];
|
|
991
|
-
lastError = "
|
|
991
|
+
lastError = "All compatible endpoints returned an empty model list";
|
|
992
992
|
} else {
|
|
993
993
|
const responding = outcome.attempts.find(({ failure }) => failure.kind !== "network");
|
|
994
994
|
activeRoot = responding?.candidate.rootUrl ?? initialRootUrl;
|
|
995
|
-
lastError = summarizeDiscoveryAttempts(outcome.attempts) || "
|
|
995
|
+
lastError = summarizeDiscoveryAttempts(outcome.attempts) || "Unknown error";
|
|
996
996
|
|
|
997
997
|
const diagnostic = await probeCliproxy(activeRoot, interaction.signal);
|
|
998
998
|
interaction.notify({
|
|
999
999
|
type: "info",
|
|
1000
|
-
message:
|
|
1000
|
+
message: `Tried ${outcome.attempts.length} compatible endpoints, but none returned a model list:\n${lastError}${diagnostic ? `\n${diagnostic}` : ""}`,
|
|
1001
1001
|
});
|
|
1002
1002
|
|
|
1003
1003
|
const choice = await interaction.prompt({
|
|
1004
1004
|
type: "select",
|
|
1005
|
-
message: "
|
|
1005
|
+
message: "Select a CLIProxy-compatible endpoint",
|
|
1006
1006
|
options: [
|
|
1007
|
-
{ id: "retry-openai", label:
|
|
1008
|
-
{ id: "anthropic", label: `Anthropic
|
|
1009
|
-
{ id: "gemini", label: `Gemini
|
|
1010
|
-
{ id: "custom", label: "
|
|
1007
|
+
{ id: "retry-openai", label: `Retry OpenAI compatible: ${openAIBaseUrl(activeRoot)}` },
|
|
1008
|
+
{ id: "anthropic", label: `Anthropic compatible: ${anthropicBaseUrl(activeRoot)}` },
|
|
1009
|
+
{ id: "gemini", label: `Gemini compatible: ${geminiBaseUrl(activeRoot)}` },
|
|
1010
|
+
{ id: "custom", label: "Custom compatible endpoint..." },
|
|
1011
1011
|
],
|
|
1012
1012
|
});
|
|
1013
1013
|
|
|
@@ -1035,7 +1035,7 @@ async function runLogin(interaction: ProviderAuthInteraction): Promise<ApiKeyCre
|
|
|
1035
1035
|
if (anthropic.kind === "unsupported") {
|
|
1036
1036
|
interaction.notify({
|
|
1037
1037
|
type: "info",
|
|
1038
|
-
message: "
|
|
1038
|
+
message: "This Anthropic-compatible endpoint has no model list endpoint. Enter model IDs manually in the next step.",
|
|
1039
1039
|
});
|
|
1040
1040
|
}
|
|
1041
1041
|
}
|
|
@@ -1054,18 +1054,18 @@ async function runLogin(interaction: ProviderAuthInteraction): Promise<ApiKeyCre
|
|
|
1054
1054
|
const customInput = (
|
|
1055
1055
|
await interaction.prompt({
|
|
1056
1056
|
type: "text",
|
|
1057
|
-
message: "
|
|
1057
|
+
message: "Custom compatible endpoint (full base URL, for example https://proxy.example.com/v1)",
|
|
1058
1058
|
placeholder: "https://proxy.example.com/v1",
|
|
1059
1059
|
})
|
|
1060
1060
|
).trim();
|
|
1061
|
-
if (!customInput) throw new Error("
|
|
1061
|
+
if (!customInput) throw new Error("A custom endpoint is required.");
|
|
1062
1062
|
const protocolChoice = await interaction.prompt({
|
|
1063
1063
|
type: "select",
|
|
1064
|
-
message: "
|
|
1064
|
+
message: "Compatible protocol for this endpoint",
|
|
1065
1065
|
options: [
|
|
1066
|
-
{ id: "openai", label: "OpenAI
|
|
1067
|
-
{ id: "anthropic", label: "Anthropic
|
|
1068
|
-
{ id: "gemini", label: "Gemini
|
|
1066
|
+
{ id: "openai", label: "OpenAI compatible (/v1/chat/completions)" },
|
|
1067
|
+
{ id: "anthropic", label: "Anthropic compatible (/v1/messages)" },
|
|
1068
|
+
{ id: "gemini", label: "Gemini compatible (/v1beta)" },
|
|
1069
1069
|
],
|
|
1070
1070
|
});
|
|
1071
1071
|
api =
|
|
@@ -1089,7 +1089,7 @@ async function runLogin(interaction: ProviderAuthInteraction): Promise<ApiKeyCre
|
|
|
1089
1089
|
if (models.length === 0) {
|
|
1090
1090
|
const manual = await interaction.prompt({
|
|
1091
1091
|
type: "text",
|
|
1092
|
-
message: "
|
|
1092
|
+
message: "No model list was retrieved. Enter model IDs manually (comma or space separated; leave empty to add none for now)",
|
|
1093
1093
|
placeholder: "claude-sonnet-4-5, gemini-2.5-pro",
|
|
1094
1094
|
});
|
|
1095
1095
|
models = parseManualModelIds(manual);
|
|
@@ -1120,19 +1120,19 @@ async function runLogin(interaction: ProviderAuthInteraction): Promise<ApiKeyCre
|
|
|
1120
1120
|
writeConfig(config);
|
|
1121
1121
|
} catch (error) {
|
|
1122
1122
|
throw new Error(
|
|
1123
|
-
`CLIProxy
|
|
1123
|
+
`Failed to write CLIProxy config: ${error instanceof Error ? error.message : String(error)} (${configPath()})`,
|
|
1124
1124
|
);
|
|
1125
1125
|
}
|
|
1126
1126
|
|
|
1127
1127
|
if (models.length > 0) {
|
|
1128
1128
|
interaction.notify({
|
|
1129
1129
|
type: "info",
|
|
1130
|
-
message: `CLIProxy
|
|
1130
|
+
message: `CLIProxy config saved: ${baseUrl} (${models.length} models, ${PROVIDER_APIS[api]}).`,
|
|
1131
1131
|
});
|
|
1132
1132
|
} else {
|
|
1133
1133
|
interaction.notify({
|
|
1134
1134
|
type: "info",
|
|
1135
|
-
message: `CLIProxy
|
|
1135
|
+
message: `CLIProxy config saved: ${baseUrl}, but no models were imported. Use /cliproxy refresh to retry.`,
|
|
1136
1136
|
});
|
|
1137
1137
|
}
|
|
1138
1138
|
|
|
@@ -1183,7 +1183,7 @@ async function refreshProviderModels(context: RefreshModelsContext): Promise<rea
|
|
|
1183
1183
|
writeConfig({
|
|
1184
1184
|
...config,
|
|
1185
1185
|
updatedAt: Date.now(),
|
|
1186
|
-
lastError: "
|
|
1186
|
+
lastError: "Upstream returned an empty model list (manual models kept)",
|
|
1187
1187
|
});
|
|
1188
1188
|
} catch {
|
|
1189
1189
|
// Best effort.
|
|
@@ -1197,7 +1197,7 @@ async function refreshProviderModels(context: RefreshModelsContext): Promise<rea
|
|
|
1197
1197
|
models: enriched,
|
|
1198
1198
|
source: "auto",
|
|
1199
1199
|
updatedAt: Date.now(),
|
|
1200
|
-
...(enriched.length === 0 ? { lastError: "
|
|
1200
|
+
...(enriched.length === 0 ? { lastError: "Upstream returned an empty model list" } : {}),
|
|
1201
1201
|
};
|
|
1202
1202
|
if (enriched.length > 0) delete updated.lastError;
|
|
1203
1203
|
try {
|
|
@@ -1254,14 +1254,14 @@ export default function (pi: ExtensionAPI): void {
|
|
|
1254
1254
|
pi.registerProvider(buildProvider(readConfig()));
|
|
1255
1255
|
|
|
1256
1256
|
pi.registerCommand("cliproxy", {
|
|
1257
|
-
description: "
|
|
1257
|
+
description: "Show CLIProxy config, or run /cliproxy refresh to refresh models",
|
|
1258
1258
|
handler: async (args, ctx) => {
|
|
1259
1259
|
const subcommand = args.trim().toLowerCase();
|
|
1260
1260
|
const config = readConfig();
|
|
1261
1261
|
|
|
1262
1262
|
if (subcommand === "refresh") {
|
|
1263
1263
|
if (!config) {
|
|
1264
|
-
ctx.ui.notify("
|
|
1264
|
+
ctx.ui.notify("CLIProxy is not configured. Run /login and choose \"Sign in with an API key\" -> CLIProxy.", "warning");
|
|
1265
1265
|
return;
|
|
1266
1266
|
}
|
|
1267
1267
|
let refreshError: string | undefined;
|
|
@@ -1277,20 +1277,20 @@ export default function (pi: ExtensionAPI): void {
|
|
|
1277
1277
|
}
|
|
1278
1278
|
const count = ctx.modelRegistry.getAll().filter((model) => model.provider === PROVIDER_ID).length;
|
|
1279
1279
|
if (refreshError) {
|
|
1280
|
-
ctx.ui.notify(`CLIProxy
|
|
1280
|
+
ctx.ui.notify(`CLIProxy model refresh failed: ${refreshError}`, "error");
|
|
1281
1281
|
} else if (lastRefreshError) {
|
|
1282
|
-
ctx.ui.notify(`CLIProxy
|
|
1282
|
+
ctx.ui.notify(`CLIProxy model refresh failed; using cached models (${count}): ${lastRefreshError}`, "warning");
|
|
1283
1283
|
} else if (count === 0) {
|
|
1284
|
-
ctx.ui.notify("CLIProxy
|
|
1284
|
+
ctx.ui.notify("CLIProxy refreshed, but no models were found. Check the API key or run /login again.", "warning");
|
|
1285
1285
|
} else {
|
|
1286
|
-
ctx.ui.notify(`CLIProxy
|
|
1286
|
+
ctx.ui.notify(`CLIProxy models refreshed: ${count}.`, "info");
|
|
1287
1287
|
}
|
|
1288
1288
|
return;
|
|
1289
1289
|
}
|
|
1290
1290
|
|
|
1291
1291
|
if (!config) {
|
|
1292
1292
|
ctx.ui.notify(
|
|
1293
|
-
|
|
1293
|
+
`CLIProxy is not configured.\nRun /login -> "Sign in with an API key" -> CLIProxy and enter the bound domain and API key.\nConfig will be written to: ${configPath()}`,
|
|
1294
1294
|
"info",
|
|
1295
1295
|
);
|
|
1296
1296
|
return;
|
|
@@ -1299,13 +1299,13 @@ export default function (pi: ExtensionAPI): void {
|
|
|
1299
1299
|
const count = ctx.modelRegistry.getAll().filter((model) => model.provider === PROVIDER_ID).length;
|
|
1300
1300
|
const lastError = lastRefreshError ?? config.lastError;
|
|
1301
1301
|
const lines = [
|
|
1302
|
-
`CLIProxy
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
lastError ?
|
|
1307
|
-
"
|
|
1308
|
-
"
|
|
1302
|
+
`CLIProxy: ${config.baseUrl}`,
|
|
1303
|
+
`Protocol: ${PROVIDER_APIS[config.api]} (${config.api})`,
|
|
1304
|
+
`Models: ${count}${config.models.length !== count ? ` (${config.models.length} cached)` : ""}`,
|
|
1305
|
+
`Config: ${configPath()}`,
|
|
1306
|
+
lastError ? `Last error: ${lastError}` : undefined,
|
|
1307
|
+
"Reconfigure: /login -> \"Sign in with an API key\" -> CLIProxy",
|
|
1308
|
+
"Refresh models: /cliproxy refresh",
|
|
1309
1309
|
].filter((line): line is string => Boolean(line));
|
|
1310
1310
|
ctx.ui.notify(lines.join("\n"), "info");
|
|
1311
1311
|
},
|