@bman654/clodex 2.18.6 → 2.18.7
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 +1 -1
- package/dist/cli.js +23 -27
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -326,7 +326,7 @@ account's model entitlement.
|
|
|
326
326
|
|
|
327
327
|
Providers supported: `openai` (API key, platform.openai.com), `openai-oauth` (ChatGPT/Codex plan), and `opencode-go` (OpenCode Go API key). OpenCode Go exposes its Anthropic Messages, Chat Completions, and Responses models; entries whose transport has not been verified against the live endpoint are left out. See [OpenCode Go provider](docs/opencode-go.md).
|
|
328
328
|
|
|
329
|
-
A **custom OpenAI-compatible server** is any OpenAI-style API, given by its base URL (the part before `/chat/completions`), such as `https://openrouter.ai/api/v1`. `providers add` asks for a name, the base URL and an API key (leave it empty for a local server without auth), lists the server's models to check the connection, and saves the provider as `custom-<name>`. Its models then appear in `clodex models`; once saved as favorites they are addressed as `clodex:custom-<name>:<model>` and can be given a short alias with `clodex models --alias`. The base URL is checked before anything is saved: plain `http://` is accepted only after you confirm it, and only
|
|
329
|
+
A **custom OpenAI-compatible server** is any OpenAI-style API, given by its base URL (the part before `/chat/completions`), such as `https://openrouter.ai/api/v1`. `providers add` asks for a name, the base URL and an API key (leave it empty for a local server without auth), lists the server's models to check the connection, and saves the provider as `custom-<name>`. Its models then appear in `clodex models`; once saved as favorites they are addressed as `clodex:custom-<name>:<model>` and can be given a short alias with `clodex models --alias`. The base URL is checked before anything is saved and again when its models are refreshed: plain `http://` is accepted only after you confirm it, and only if every address the host resolves to is on this machine, your LAN or a private network such as Tailscale (loopback, private, unique-local IPv6 or carrier-grade-NAT `100.64.0.0/10` addresses); `https://` is accepted on any of those networks as well as the public internet; and a link-local (`169.254.0.0/16`, `fe80::/10`) or well-known cloud-metadata address is refused whether or not HTTP was approved. A server on another machine in your tailnet can be added by its `100.x` address, its MagicDNS name, or the HTTPS name `tailscale serve` gives it. A model's id and name are stored with surrounding whitespace trimmed, and a model whose trimmed id or name still contains a control character is skipped, whether the list is fetched by `providers add` or by `providers refresh-models`, and error text from the server is shown with control characters replaced by spaces. Only the OpenAI-compatible kind is offered in the menu.
|
|
330
330
|
|
|
331
331
|
### `clodex install-vscode-launcher`
|
|
332
332
|
|
package/dist/cli.js
CHANGED
|
@@ -346,7 +346,7 @@ import { join } from "path";
|
|
|
346
346
|
// package.json
|
|
347
347
|
var package_default = {
|
|
348
348
|
name: "@bman654/clodex",
|
|
349
|
-
version: "2.18.
|
|
349
|
+
version: "2.18.7",
|
|
350
350
|
publishConfig: {
|
|
351
351
|
access: "public"
|
|
352
352
|
},
|
|
@@ -16009,21 +16009,17 @@ var BLOCKED_HOSTNAMES = /* @__PURE__ */ new Set([
|
|
|
16009
16009
|
"169.254.169.254",
|
|
16010
16010
|
"metadata.google.internal",
|
|
16011
16011
|
"169.254.170.2",
|
|
16012
|
-
"fd00:ec2::254"
|
|
16012
|
+
"fd00:ec2::254",
|
|
16013
|
+
"100.100.100.200",
|
|
16014
|
+
// Alibaba Cloud ECS
|
|
16015
|
+
"fd20:ce::254"
|
|
16016
|
+
// Google Cloud IPv6
|
|
16013
16017
|
]);
|
|
16014
|
-
|
|
16018
|
+
var NON_PUBLIC_RANGES = /* @__PURE__ */ new Set(["loopback", "private", "uniqueLocal", "carrierGradeNat"]);
|
|
16019
|
+
function isBlockedIp(ipStr) {
|
|
16015
16020
|
try {
|
|
16016
16021
|
const ip = ipaddr.process(ipStr);
|
|
16017
|
-
|
|
16018
|
-
if (allowInsecureLocal && (range === "loopback" || range === "private")) {
|
|
16019
|
-
return false;
|
|
16020
|
-
}
|
|
16021
|
-
if (range === "loopback") return true;
|
|
16022
|
-
if (range === "private") return true;
|
|
16023
|
-
if (range === "linkLocal") return true;
|
|
16024
|
-
if (range === "uniqueLocal") return true;
|
|
16025
|
-
if (range === "carrierGradeNat") return true;
|
|
16026
|
-
return false;
|
|
16022
|
+
return ip.range() === "linkLocal" || BLOCKED_HOSTNAMES.has(ip.toString());
|
|
16027
16023
|
} catch {
|
|
16028
16024
|
return true;
|
|
16029
16025
|
}
|
|
@@ -16057,10 +16053,10 @@ async function validateCustomEndpointUrl(rawUrl, opts = {}) {
|
|
|
16057
16053
|
return {
|
|
16058
16054
|
ok: false,
|
|
16059
16055
|
error: "Only HTTPS URLs are allowed.",
|
|
16060
|
-
hint: "For
|
|
16056
|
+
hint: "For a server on this machine, your LAN or Tailscale (Ollama, LM Studio, vLLM), allow insecure HTTP when prompted."
|
|
16061
16057
|
};
|
|
16062
16058
|
} else if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
16063
|
-
return { ok: false, error: "URL must use https:// or user-approved http:// for local
|
|
16059
|
+
return { ok: false, error: "URL must use https:// or user-approved http:// for local, LAN or Tailscale servers." };
|
|
16064
16060
|
}
|
|
16065
16061
|
const rawHostname = parsed.hostname.toLowerCase();
|
|
16066
16062
|
const hostname = rawHostname.replace(/^\[(.*)\]$/, "$1");
|
|
@@ -16068,7 +16064,7 @@ async function validateCustomEndpointUrl(rawUrl, opts = {}) {
|
|
|
16068
16064
|
return {
|
|
16069
16065
|
ok: false,
|
|
16070
16066
|
error: "This URL points to a blocked internal/metadata host.",
|
|
16071
|
-
hint: "Use
|
|
16067
|
+
hint: "Use your model server's own loopback, LAN, Tailscale or public address."
|
|
16072
16068
|
};
|
|
16073
16069
|
}
|
|
16074
16070
|
const addresses = await resolveHostAddresses(hostname);
|
|
@@ -16085,19 +16081,18 @@ async function validateCustomEndpointUrl(rawUrl, opts = {}) {
|
|
|
16085
16081
|
} catch {
|
|
16086
16082
|
continue;
|
|
16087
16083
|
}
|
|
16088
|
-
if (isBlockedIp(addr
|
|
16084
|
+
if (isBlockedIp(addr)) {
|
|
16089
16085
|
return {
|
|
16090
16086
|
ok: false,
|
|
16091
|
-
error: "URL resolves to a
|
|
16092
|
-
hint: "
|
|
16087
|
+
error: "URL resolves to a restricted link-local or cloud metadata address.",
|
|
16088
|
+
hint: "Link-local addresses are not supported. Use the server's loopback, LAN, Tailscale or public address."
|
|
16093
16089
|
};
|
|
16094
16090
|
}
|
|
16095
16091
|
}
|
|
16096
16092
|
if (parsed.protocol === "http:") {
|
|
16097
16093
|
const allResolvedAddressesAreLocal = addresses.every((addr) => {
|
|
16098
16094
|
try {
|
|
16099
|
-
|
|
16100
|
-
return range === "loopback" || range === "private" || range === "uniqueLocal";
|
|
16095
|
+
return NON_PUBLIC_RANGES.has(ipaddr.process(addr).range());
|
|
16101
16096
|
} catch {
|
|
16102
16097
|
return false;
|
|
16103
16098
|
}
|
|
@@ -16105,8 +16100,8 @@ async function validateCustomEndpointUrl(rawUrl, opts = {}) {
|
|
|
16105
16100
|
if (!allowLocal || !allResolvedAddressesAreLocal) {
|
|
16106
16101
|
return {
|
|
16107
16102
|
ok: false,
|
|
16108
|
-
error: "HTTP is only allowed for
|
|
16109
|
-
hint: "Use https
|
|
16103
|
+
error: "HTTP is only allowed for addresses on this machine, your LAN or a private network such as Tailscale.",
|
|
16104
|
+
hint: "Use https:// for a server on the public internet."
|
|
16110
16105
|
};
|
|
16111
16106
|
}
|
|
16112
16107
|
}
|
|
@@ -16354,9 +16349,9 @@ async function runCustomEndpointAddFlow(onResult) {
|
|
|
16354
16349
|
}
|
|
16355
16350
|
let allowInsecureLocal = false;
|
|
16356
16351
|
if (/^http:\/\//i.test(String(baseUrl).trim())) {
|
|
16357
|
-
p3.log.warn("HTTP is not encrypted. Only use it for a trusted
|
|
16352
|
+
p3.log.warn("HTTP is not encrypted. Only use it for a trusted server on this machine, your LAN or Tailscale.");
|
|
16358
16353
|
const allow = await p3.confirm({
|
|
16359
|
-
message: "Allow insecure HTTP for this local
|
|
16354
|
+
message: "Allow insecure HTTP for this local, LAN or Tailscale server?",
|
|
16360
16355
|
initialValue: true
|
|
16361
16356
|
});
|
|
16362
16357
|
if (p3.isCancel(allow)) {
|
|
@@ -16879,8 +16874,9 @@ async function refreshApiListProvider(provider, apiKey) {
|
|
|
16879
16874
|
let safeBaseUrl = baseUrl;
|
|
16880
16875
|
const templateDefault = catalogTemplate?.defaultBaseUrl?.trim();
|
|
16881
16876
|
if (configuredUrl && configuredUrl !== templateDefault) {
|
|
16877
|
+
const customEndpoint = provider.templateId === "custom-openai" || provider.templateId === "custom-anthropic";
|
|
16882
16878
|
const urlCheck = await validateCustomEndpointUrl(baseUrl, {
|
|
16883
|
-
allowInsecureLocal: catalogTemplate?.apiKeyOptional === true
|
|
16879
|
+
allowInsecureLocal: catalogTemplate?.apiKeyOptional === true || customEndpoint
|
|
16884
16880
|
});
|
|
16885
16881
|
if (!urlCheck.ok || !urlCheck.normalizedUrl) {
|
|
16886
16882
|
return { models: [], error: `${urlCheck.error ?? "Invalid API base URL."} ${urlCheck.hint ?? ""}`.trim() };
|
|
@@ -17078,7 +17074,7 @@ async function refreshProviderModels(providerId, apiKey, registry, credentialSna
|
|
|
17078
17074
|
}
|
|
17079
17075
|
} else {
|
|
17080
17076
|
const template = resolveProviderTemplate(provider);
|
|
17081
|
-
const keyOptional = template?.apiKeyOptional === true;
|
|
17077
|
+
const keyOptional = template?.apiKeyOptional === true || isAnonymousProvider(provider);
|
|
17082
17078
|
const effectiveKey = keyOptional && isLikelyPlaceholderKey(apiKey) ? "" : apiKey;
|
|
17083
17079
|
if (!keyOptional && isLikelyPlaceholderKey(effectiveKey)) {
|
|
17084
17080
|
if (cachedModelCount(cacheProvider) > 0) {
|