@f5xc-salesdemos/xcsh 17.1.0 → 17.1.2
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/package.json +7 -7
- package/src/config/model-registry.ts +78 -1
- package/src/main.ts +13 -0
- package/src/sdk.ts +10 -3
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@f5xc-salesdemos/xcsh",
|
|
4
|
-
"version": "17.1.
|
|
4
|
+
"version": "17.1.2",
|
|
5
5
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://github.com/f5xc-salesdemos/xcsh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -46,12 +46,12 @@
|
|
|
46
46
|
"dependencies": {
|
|
47
47
|
"@agentclientprotocol/sdk": "0.16.1",
|
|
48
48
|
"@mozilla/readability": "^0.6",
|
|
49
|
-
"@f5xc-salesdemos/xcsh-stats": "17.1.
|
|
50
|
-
"@f5xc-salesdemos/pi-agent-core": "17.1.
|
|
51
|
-
"@f5xc-salesdemos/pi-ai": "17.1.
|
|
52
|
-
"@f5xc-salesdemos/pi-natives": "17.1.
|
|
53
|
-
"@f5xc-salesdemos/pi-tui": "17.1.
|
|
54
|
-
"@f5xc-salesdemos/pi-utils": "17.1.
|
|
49
|
+
"@f5xc-salesdemos/xcsh-stats": "17.1.2",
|
|
50
|
+
"@f5xc-salesdemos/pi-agent-core": "17.1.2",
|
|
51
|
+
"@f5xc-salesdemos/pi-ai": "17.1.2",
|
|
52
|
+
"@f5xc-salesdemos/pi-natives": "17.1.2",
|
|
53
|
+
"@f5xc-salesdemos/pi-tui": "17.1.2",
|
|
54
|
+
"@f5xc-salesdemos/pi-utils": "17.1.2",
|
|
55
55
|
"@sinclair/typebox": "^0.34",
|
|
56
56
|
"@xterm/headless": "^6.0",
|
|
57
57
|
"ajv": "^8.18",
|
|
@@ -247,7 +247,12 @@ const ModelOverrideSchema = Type.Object({
|
|
|
247
247
|
type ModelOverride = Static<typeof ModelOverrideSchema>;
|
|
248
248
|
|
|
249
249
|
const ProviderDiscoverySchema = Type.Object({
|
|
250
|
-
type: Type.Union([
|
|
250
|
+
type: Type.Union([
|
|
251
|
+
Type.Literal("ollama"),
|
|
252
|
+
Type.Literal("llama.cpp"),
|
|
253
|
+
Type.Literal("lm-studio"),
|
|
254
|
+
Type.Literal("openai-compat"),
|
|
255
|
+
]),
|
|
251
256
|
});
|
|
252
257
|
|
|
253
258
|
const ProviderAuthSchema = Type.Union([Type.Literal("apiKey"), Type.Literal("none")]);
|
|
@@ -831,6 +836,29 @@ export class ModelRegistry {
|
|
|
831
836
|
this.#backgroundRefresh = refreshPromise;
|
|
832
837
|
}
|
|
833
838
|
|
|
839
|
+
/**
|
|
840
|
+
* Await the in-flight background refresh if one is running.
|
|
841
|
+
* Returns immediately if no background refresh is in progress.
|
|
842
|
+
*/
|
|
843
|
+
async awaitBackgroundRefresh(): Promise<void> {
|
|
844
|
+
if (this.#backgroundRefresh) {
|
|
845
|
+
await this.#backgroundRefresh;
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
/**
|
|
850
|
+
* Check if any non-optional discoverable provider has no cached models yet.
|
|
851
|
+
* Returns true on first run when the model cache is empty.
|
|
852
|
+
*/
|
|
853
|
+
hasUncachedDiscoverableProviders(): boolean {
|
|
854
|
+
for (const [, state] of this.#providerDiscoveryStates) {
|
|
855
|
+
if (state.status === "idle" && !state.optional) {
|
|
856
|
+
return true;
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
return false;
|
|
860
|
+
}
|
|
861
|
+
|
|
834
862
|
async refreshProvider(providerId: string, strategy: ModelRefreshStrategy = "online"): Promise<void> {
|
|
835
863
|
this.#reloadStaticModels();
|
|
836
864
|
for (const selector of this.#suppressedSelectors.keys()) {
|
|
@@ -1285,6 +1313,8 @@ export class ModelRegistry {
|
|
|
1285
1313
|
return this.#discoverLlamaCppModels(providerConfig);
|
|
1286
1314
|
case "lm-studio":
|
|
1287
1315
|
return this.#discoverLmStudioModels(providerConfig);
|
|
1316
|
+
case "openai-compat":
|
|
1317
|
+
return this.#discoverOpenAICompatModels(providerConfig);
|
|
1288
1318
|
}
|
|
1289
1319
|
}
|
|
1290
1320
|
|
|
@@ -1646,6 +1676,53 @@ export class ModelRegistry {
|
|
|
1646
1676
|
return this.#applyProviderModelOverrides(providerConfig.provider, discovered);
|
|
1647
1677
|
}
|
|
1648
1678
|
|
|
1679
|
+
async #discoverOpenAICompatModels(providerConfig: DiscoveryProviderConfig): Promise<Model<Api>[]> {
|
|
1680
|
+
const baseUrl = (providerConfig.baseUrl ?? "").replace(/\/+$/, "");
|
|
1681
|
+
if (!baseUrl) {
|
|
1682
|
+
throw new Error("openai-compat discovery requires a baseUrl");
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
const headers: Record<string, string> = { ...(providerConfig.headers ?? {}) };
|
|
1686
|
+
const apiKey =
|
|
1687
|
+
this.#customProviderApiKeys.get(providerConfig.provider) ??
|
|
1688
|
+
(await this.#peekApiKeyForProvider(providerConfig.provider));
|
|
1689
|
+
if (apiKey && apiKey !== DEFAULT_LOCAL_TOKEN && apiKey !== kNoAuth) {
|
|
1690
|
+
headers.Authorization = `Bearer ${apiKey}`;
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
const modelsUrl = `${baseUrl}/models`;
|
|
1694
|
+
const response = await fetch(modelsUrl, {
|
|
1695
|
+
headers: { Accept: "application/json", ...headers },
|
|
1696
|
+
signal: AbortSignal.timeout(3000),
|
|
1697
|
+
});
|
|
1698
|
+
if (!response.ok) {
|
|
1699
|
+
throw new Error(`HTTP ${response.status} from ${modelsUrl}`);
|
|
1700
|
+
}
|
|
1701
|
+
const payload = (await response.json()) as { data?: Array<{ id: string }> };
|
|
1702
|
+
const items = payload.data ?? [];
|
|
1703
|
+
const discovered: Model<Api>[] = [];
|
|
1704
|
+
for (const item of items) {
|
|
1705
|
+
const id = item.id;
|
|
1706
|
+
if (!id) continue;
|
|
1707
|
+
discovered.push(
|
|
1708
|
+
enrichModelThinking({
|
|
1709
|
+
id,
|
|
1710
|
+
name: id,
|
|
1711
|
+
api: providerConfig.api,
|
|
1712
|
+
provider: providerConfig.provider,
|
|
1713
|
+
baseUrl,
|
|
1714
|
+
reasoning: false,
|
|
1715
|
+
input: ["text"],
|
|
1716
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
1717
|
+
contextWindow: 128000,
|
|
1718
|
+
maxTokens: 8192,
|
|
1719
|
+
headers,
|
|
1720
|
+
}),
|
|
1721
|
+
);
|
|
1722
|
+
}
|
|
1723
|
+
return this.#applyProviderModelOverrides(providerConfig.provider, discovered);
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1649
1726
|
#normalizeLlamaCppBaseUrl(baseUrl?: string): string {
|
|
1650
1727
|
const defaultBaseUrl = "http://127.0.0.1:8080";
|
|
1651
1728
|
const raw = baseUrl || defaultBaseUrl;
|
package/src/main.ts
CHANGED
|
@@ -624,6 +624,19 @@ export async function runRootCommand(parsed: Args, rawArgs: string[]): Promise<v
|
|
|
624
624
|
|
|
625
625
|
const cwd = getProjectDir();
|
|
626
626
|
await logger.time("settings:init", Settings.init, { cwd });
|
|
627
|
+
|
|
628
|
+
// F5 XC profile loading — optional, never blocks startup.
|
|
629
|
+
// NOTE: This runs in the CLI path only. SDK consumers using createAgentSession()
|
|
630
|
+
// directly must call ProfileService.init(configDir).loadActive() themselves.
|
|
631
|
+
try {
|
|
632
|
+
const { ProfileService } = await import("./services/f5xc-profile");
|
|
633
|
+
const { getF5XCConfigDir } = await import("@f5xc-salesdemos/pi-utils");
|
|
634
|
+
const profileService = ProfileService.init(getF5XCConfigDir());
|
|
635
|
+
await profileService.loadActive();
|
|
636
|
+
} catch {
|
|
637
|
+
// F5 XC auth is optional — silently continue if anything fails
|
|
638
|
+
}
|
|
639
|
+
|
|
627
640
|
if (parsedArgs.mode === "rpc") {
|
|
628
641
|
applyRpcDefaultSettingOverrides();
|
|
629
642
|
}
|
package/src/sdk.ts
CHANGED
|
@@ -23,11 +23,11 @@ import {
|
|
|
23
23
|
prompt,
|
|
24
24
|
Snowflake,
|
|
25
25
|
} from "@f5xc-salesdemos/pi-utils";
|
|
26
|
-
import chalk from "chalk";
|
|
27
26
|
import { AsyncJobManager, isBackgroundJobSupportEnabled } from "./async";
|
|
28
27
|
import { createAutoresearchExtension } from "./autoresearch";
|
|
29
28
|
import { loadCapability } from "./capability";
|
|
30
29
|
import { type Rule, ruleCapability } from "./capability/rule";
|
|
30
|
+
import { hasLiteLLMEnv } from "./config/auto-config";
|
|
31
31
|
import { ModelRegistry } from "./config/model-registry";
|
|
32
32
|
import { formatModelString, parseModelPattern, parseModelString, resolveModelRoleValue } from "./config/model-resolver";
|
|
33
33
|
import { loadPromptTemplates as loadPromptTemplatesInternal, type PromptTemplate } from "./config/prompt-templates";
|
|
@@ -780,6 +780,13 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
780
780
|
const modelMatchPreferences = {
|
|
781
781
|
usageOrder: settings.getStorage()?.getModelUsageOrder(),
|
|
782
782
|
};
|
|
783
|
+
// When LiteLLM is configured and no model cache exists yet (first run),
|
|
784
|
+
// await the background refresh so model discovery from the proxy completes
|
|
785
|
+
// before we select a default model. Bounded by the 3s probe timeout.
|
|
786
|
+
if (!options.modelRegistry && hasLiteLLMEnv() && modelRegistry.hasUncachedDiscoverableProviders()) {
|
|
787
|
+
await logger.time("awaitLiteLLMDiscovery", () => modelRegistry.awaitBackgroundRefresh());
|
|
788
|
+
}
|
|
789
|
+
|
|
783
790
|
const defaultRoleSpec = logger.time("resolveDefaultModelRole", () =>
|
|
784
791
|
resolveModelRoleValue(settings.getModelRole("default"), modelRegistry.getAvailable(), {
|
|
785
792
|
settings,
|
|
@@ -1069,8 +1076,8 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1069
1076
|
if (enableMCP) {
|
|
1070
1077
|
const mcpResult = await logger.time("discoverAndLoadMCPTools", discoverAndLoadMCPTools, cwd, {
|
|
1071
1078
|
onConnecting: serverNames => {
|
|
1072
|
-
if (
|
|
1073
|
-
|
|
1079
|
+
if (serverNames.length > 0) {
|
|
1080
|
+
logger.debug("Connecting to MCP servers", { servers: serverNames });
|
|
1074
1081
|
}
|
|
1075
1082
|
},
|
|
1076
1083
|
enableProjectConfig: settings.get("mcp.enableProjectConfig") ?? true,
|