@pellux/goodvibes-agent 1.1.7 → 1.2.0
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/CHANGELOG.md +170 -0
- package/README.md +46 -30
- package/dist/package/{ast-grep-napi.linux-x64-gnu-mkk8xwww.node → ast-grep-napi.linux-x64-gnu-swtppvy9.node} +0 -0
- package/dist/package/{ast-grep-napi.linux-x64-musl-ryqtgdv6.node → ast-grep-napi.linux-x64-musl-ttfcdtap.node} +0 -0
- package/dist/package/main.js +132577 -91483
- package/docs/README.md +14 -7
- package/docs/channels-remote-and-api.md +8 -5
- package/docs/connected-host.md +14 -12
- package/docs/getting-started.md +56 -30
- package/docs/knowledge-artifacts-and-multimodal.md +7 -5
- package/docs/project-planning.md +2 -2
- package/docs/providers-and-routing.md +11 -3
- package/docs/tools-and-commands.md +114 -45
- package/docs/voice-and-live-tts.md +3 -1
- package/package.json +3 -2
- package/release/release-notes.md +130 -5
- package/release/release-readiness.json +120 -90
- package/src/agent/assistant-cockpit.ts +247 -0
- package/src/agent/autonomy-schedule-format.ts +96 -0
- package/src/agent/autonomy-schedule.ts +514 -0
- package/src/agent/channel-delivery-receipts.ts +292 -0
- package/src/agent/competitive-feature-inventory.ts +296 -0
- package/src/agent/document-registry-types.ts +106 -0
- package/src/agent/document-registry.ts +734 -0
- package/src/agent/harness-control.ts +2 -2
- package/src/agent/memory-prompt.ts +2 -2
- package/src/agent/operator-actions.ts +50 -6
- package/src/agent/persona-registry.ts +8 -0
- package/src/agent/project-context-files.ts +273 -0
- package/src/agent/prompt-context-receipts.ts +502 -0
- package/src/agent/reminder-schedule-format.ts +16 -2
- package/src/agent/research-run-registry.ts +582 -0
- package/src/agent/research-source-registry.ts +441 -0
- package/src/agent/routine-registry.ts +19 -3
- package/src/agent/routine-schedule-format.ts +27 -2
- package/src/agent/runtime-profile-starters.ts +7 -0
- package/src/agent/runtime-profile.ts +106 -4
- package/src/agent/schedule-edit-format.ts +129 -0
- package/src/agent/schedule-edit.ts +496 -0
- package/src/agent/schedule-next-routes.ts +42 -0
- package/src/agent/setup-wizard-checkpoint.ts +140 -0
- package/src/agent/setup-wizard.ts +447 -0
- package/src/agent/skill-registry-types.ts +102 -0
- package/src/agent/skill-registry.ts +81 -107
- package/src/agent/vibe-confirmation-routes.ts +62 -0
- package/src/agent/vibe-file.ts +285 -0
- package/src/cli/agent-knowledge-command.ts +45 -2
- package/src/cli/help.ts +3 -0
- package/src/cli/profiles-command.ts +25 -11
- package/src/config/agent-settings-policy.ts +1 -18
- package/src/input/agent-workspace-activation.ts +53 -0
- package/src/input/agent-workspace-artifact-browser-editor.ts +494 -0
- package/src/input/agent-workspace-artifact-metadata.ts +17 -0
- package/src/input/agent-workspace-basic-command-editors.ts +5 -1
- package/src/input/agent-workspace-categories.ts +153 -155
- package/src/input/agent-workspace-channel-triage.ts +481 -0
- package/src/input/agent-workspace-channels.ts +263 -0
- package/src/input/agent-workspace-command-editor.ts +152 -0
- package/src/input/agent-workspace-context-snapshot.ts +349 -0
- package/src/input/agent-workspace-delegation-editor-submission.ts +8 -0
- package/src/input/agent-workspace-document-editor.ts +502 -0
- package/src/input/agent-workspace-document-ops-editor.ts +523 -0
- package/src/input/agent-workspace-host-category.ts +1 -1
- package/src/input/agent-workspace-local-library-snapshot.ts +167 -0
- package/src/input/agent-workspace-model-compare-editor.ts +376 -0
- package/src/input/agent-workspace-model-compare-prompt-submission.ts +552 -0
- package/src/input/agent-workspace-model-compare-types.ts +82 -0
- package/src/input/agent-workspace-model-compare-utils.ts +35 -0
- package/src/input/agent-workspace-onboarding-categories.ts +141 -0
- package/src/input/agent-workspace-operations-command-editor-submission.ts +53 -0
- package/src/input/agent-workspace-operations-command-editors.ts +22 -0
- package/src/input/agent-workspace-research-report-editor.ts +212 -0
- package/src/input/agent-workspace-research-run-editor.ts +144 -0
- package/src/input/agent-workspace-research-source-editor.ts +167 -0
- package/src/input/agent-workspace-review-packet-snapshot.ts +593 -0
- package/src/input/agent-workspace-review-packet-utils.ts +265 -0
- package/src/input/agent-workspace-settings.ts +262 -30
- package/src/input/agent-workspace-setup-checkpoint-action.ts +127 -0
- package/src/input/agent-workspace-setup-snapshot.ts +198 -0
- package/src/input/agent-workspace-setup.ts +34 -1
- package/src/input/agent-workspace-snapshot.ts +162 -147
- package/src/input/agent-workspace-types.ts +287 -2
- package/src/input/agent-workspace.ts +18 -0
- package/src/input/command-registry.ts +9 -2
- package/src/input/commands/agent-runtime-profile-runtime.ts +28 -13
- package/src/input/commands/channels-runtime.ts +102 -114
- package/src/input/commands/delegation-runtime.ts +87 -24
- package/src/input/commands/knowledge-browser-flags.ts +12 -0
- package/src/input/commands/knowledge.ts +28 -14
- package/src/input/commands/operator-actions-runtime.ts +6 -3
- package/src/input/commands/schedule-runtime.ts +71 -14
- package/src/input/commands/session-content.ts +2 -2
- package/src/input/commands/shell-core.ts +2 -0
- package/src/input/commands/vibe-runtime.ts +140 -0
- package/src/input/commands.ts +2 -0
- package/src/input/connected-host-routes.ts +123 -0
- package/src/input/settings-modal-types.ts +14 -0
- package/src/main.ts +15 -15
- package/src/panels/qr-panel.ts +2 -2
- package/src/renderer/agent-workspace-context-lines.ts +686 -0
- package/src/renderer/agent-workspace.ts +30 -419
- package/src/renderer/settings-modal-helpers.ts +12 -0
- package/src/renderer/settings-modal.ts +12 -0
- package/src/runtime/bootstrap-command-context.ts +11 -1
- package/src/runtime/bootstrap-command-parts.ts +14 -3
- package/src/runtime/bootstrap-core.ts +39 -1
- package/src/runtime/bootstrap-shell.ts +6 -0
- package/src/runtime/bootstrap.ts +113 -30
- package/src/runtime/connected-host-auth.ts +3 -2
- package/src/runtime/execution-ledger.ts +231 -0
- package/src/runtime/services.ts +88 -10
- package/src/runtime/tool-permission-safety.ts +199 -5
- package/src/shell/session-continuity-hints.ts +39 -0
- package/src/tools/agent-artifacts-tool.ts +606 -0
- package/src/tools/agent-audit-tool.ts +155 -0
- package/src/tools/agent-autonomy-schedule-tool.ts +238 -0
- package/src/tools/agent-autonomy-tool.ts +140 -0
- package/src/tools/agent-channel-send-tool.ts +24 -2
- package/src/tools/agent-channels-tool.ts +140 -0
- package/src/tools/agent-computer-tool.ts +168 -0
- package/src/tools/agent-context-policy.ts +137 -7
- package/src/tools/agent-context-tool.ts +143 -0
- package/src/tools/agent-delegation-tool.ts +128 -0
- package/src/tools/agent-device-tool.ts +240 -0
- package/src/tools/agent-documents-tool.ts +684 -0
- package/src/tools/agent-execution-tool.ts +230 -0
- package/src/tools/agent-harness-agent-orchestration.ts +703 -0
- package/src/tools/agent-harness-autonomy-intake.ts +785 -0
- package/src/tools/agent-harness-autonomy-live-records.ts +588 -0
- package/src/tools/agent-harness-autonomy-queue-types.ts +88 -0
- package/src/tools/agent-harness-autonomy-queue.ts +560 -0
- package/src/tools/agent-harness-background-processes-types.ts +35 -0
- package/src/tools/agent-harness-background-processes.ts +794 -0
- package/src/tools/agent-harness-browser-cockpit-route.ts +219 -0
- package/src/tools/agent-harness-browser-control.ts +338 -0
- package/src/tools/agent-harness-channel-metadata.ts +168 -10
- package/src/tools/agent-harness-cli-command-policy.ts +110 -0
- package/src/tools/agent-harness-command-catalog.ts +4 -2
- package/src/tools/agent-harness-command-runner.ts +42 -0
- package/src/tools/agent-harness-connected-host-status.ts +7 -4
- package/src/tools/agent-harness-delegation-posture.ts +234 -19
- package/src/tools/agent-harness-document-ops-reviewer-readiness.ts +234 -0
- package/src/tools/agent-harness-document-ops-types.ts +72 -0
- package/src/tools/agent-harness-document-ops.ts +671 -0
- package/src/tools/agent-harness-execution-history.ts +489 -0
- package/src/tools/agent-harness-execution-posture.ts +382 -0
- package/src/tools/agent-harness-file-recovery.ts +135 -0
- package/src/tools/agent-harness-keybinding-metadata.ts +16 -12
- package/src/tools/agent-harness-learning-curator-common.ts +102 -0
- package/src/tools/agent-harness-learning-curator-consolidation.ts +295 -0
- package/src/tools/agent-harness-learning-curator-proposals.ts +606 -0
- package/src/tools/agent-harness-learning-curator-types.ts +151 -0
- package/src/tools/agent-harness-learning-curator.ts +417 -0
- package/src/tools/agent-harness-local-model-benchmarks.ts +199 -0
- package/src/tools/agent-harness-local-model-cookbook.ts +238 -0
- package/src/tools/agent-harness-local-model-endpoints.ts +673 -0
- package/src/tools/agent-harness-media-posture.ts +208 -2
- package/src/tools/agent-harness-memory-posture.ts +556 -0
- package/src/tools/agent-harness-metadata.ts +100 -168
- package/src/tools/agent-harness-mode-catalog.ts +84 -33
- package/src/tools/agent-harness-model-catalog.ts +162 -0
- package/src/tools/agent-harness-model-provider-health.ts +186 -0
- package/src/tools/agent-harness-model-readiness.ts +406 -0
- package/src/tools/agent-harness-model-routing-types.ts +266 -0
- package/src/tools/agent-harness-model-routing-utils.ts +30 -0
- package/src/tools/agent-harness-model-routing.ts +137 -190
- package/src/tools/agent-harness-operator-methods.ts +115 -133
- package/src/tools/agent-harness-pairing-posture.ts +431 -18
- package/src/tools/agent-harness-personal-ops-discovery.ts +533 -0
- package/src/tools/agent-harness-personal-ops-intake.ts +385 -0
- package/src/tools/agent-harness-personal-ops-lanes.ts +204 -0
- package/src/tools/agent-harness-personal-ops-records.ts +732 -0
- package/src/tools/agent-harness-personal-ops-runner.ts +637 -0
- package/src/tools/agent-harness-personal-ops-types.ts +222 -0
- package/src/tools/agent-harness-personal-ops.ts +606 -0
- package/src/tools/agent-harness-project-context.ts +144 -0
- package/src/tools/agent-harness-prompt-context.ts +589 -0
- package/src/tools/agent-harness-provider-account-metadata.ts +2 -2
- package/src/tools/agent-harness-release-evidence.ts +9 -7
- package/src/tools/agent-harness-release-readiness.ts +9 -7
- package/src/tools/agent-harness-research-briefing.ts +427 -0
- package/src/tools/agent-harness-research-queue.ts +250 -0
- package/src/tools/agent-harness-research-runs.ts +295 -0
- package/src/tools/agent-harness-research-workflow.ts +322 -0
- package/src/tools/agent-harness-security-posture.ts +2 -2
- package/src/tools/agent-harness-service-posture.ts +9 -9
- package/src/tools/agent-harness-session-metadata.ts +3 -3
- package/src/tools/agent-harness-setup-connected-host.ts +501 -0
- package/src/tools/agent-harness-setup-describe.ts +557 -0
- package/src/tools/agent-harness-setup-handoffs.ts +502 -0
- package/src/tools/agent-harness-setup-model-helpers.ts +101 -0
- package/src/tools/agent-harness-setup-plan.ts +253 -0
- package/src/tools/agent-harness-setup-posture-types.ts +218 -0
- package/src/tools/agent-harness-setup-posture-utils.ts +173 -0
- package/src/tools/agent-harness-setup-posture.ts +449 -221
- package/src/tools/agent-harness-setup-smoke.ts +508 -0
- package/src/tools/agent-harness-sudo-posture.ts +114 -0
- package/src/tools/agent-harness-tool-schema.ts +120 -7
- package/src/tools/agent-harness-tool-types.ts +78 -0
- package/src/tools/agent-harness-tool-utils.ts +43 -0
- package/src/tools/agent-harness-tool.ts +311 -370
- package/src/tools/agent-harness-ui-surface-metadata.ts +76 -69
- package/src/tools/agent-harness-ui-surface-types.ts +46 -0
- package/src/tools/agent-harness-vibe-health.ts +105 -0
- package/src/tools/agent-harness-workspace-action-runner.ts +149 -0
- package/src/tools/agent-harness-workspace-actions.ts +114 -8
- package/src/tools/agent-harness-workspace-editor-execution.ts +214 -0
- package/src/tools/agent-harness-workspace-editor-runner.ts +755 -0
- package/src/tools/agent-host-tool.ts +159 -0
- package/src/tools/agent-knowledge-ingest-tool.ts +34 -1
- package/src/tools/agent-learning-consolidation-core.ts +327 -0
- package/src/tools/agent-learning-consolidation-tool.ts +758 -0
- package/src/tools/agent-memory-tool.ts +214 -0
- package/src/tools/agent-model-compare-export.ts +315 -0
- package/src/tools/agent-model-compare-handoff.ts +592 -0
- package/src/tools/agent-model-compare-judgment.ts +633 -0
- package/src/tools/agent-model-compare-run.ts +722 -0
- package/src/tools/agent-model-compare-tool.ts +698 -0
- package/src/tools/agent-model-compare-types.ts +191 -0
- package/src/tools/agent-model-compare-utils.ts +93 -0
- package/src/tools/agent-models-tool.ts +208 -0
- package/src/tools/agent-operator-action-tool.ts +1 -1
- package/src/tools/agent-operator-method-tool.ts +643 -0
- package/src/tools/agent-personal-ops-tool.ts +197 -0
- package/src/tools/agent-policy-explanation.ts +415 -0
- package/src/tools/agent-research-report-tool.ts +608 -0
- package/src/tools/agent-research-runs-tool.ts +434 -0
- package/src/tools/agent-research-sources-tool.ts +443 -0
- package/src/tools/agent-research-tool.ts +687 -0
- package/src/tools/agent-review-packet-presets-core.ts +757 -0
- package/src/tools/agent-review-packet-presets-tool.ts +466 -0
- package/src/tools/agent-review-packet-share-tool.ts +305 -0
- package/src/tools/agent-route-planner-candidates-setup.ts +411 -0
- package/src/tools/agent-route-planner-candidates-surfaces.ts +432 -0
- package/src/tools/agent-route-planner-candidates-work.ts +251 -0
- package/src/tools/agent-route-planner-helpers.ts +667 -0
- package/src/tools/agent-route-planner.ts +185 -0
- package/src/tools/agent-route-tool.ts +105 -0
- package/src/tools/agent-schedule-edit-tool.ts +210 -0
- package/src/tools/agent-schedule-tool.ts +282 -0
- package/src/tools/agent-security-tool.ts +145 -0
- package/src/tools/agent-sessions-tool.ts +122 -0
- package/src/tools/agent-settings-import-tool.ts +104 -0
- package/src/tools/agent-settings-tool.ts +176 -0
- package/src/tools/agent-setup-tool.ts +226 -0
- package/src/tools/agent-support-tool.ts +122 -0
- package/src/tools/agent-terminal-process-tools.ts +167 -0
- package/src/tools/agent-tool-policy-guard-types.ts +77 -0
- package/src/tools/agent-tool-policy-guard.ts +106 -85
- package/src/tools/agent-vibe-tool.ts +297 -0
- package/src/tools/agent-work-plan-tool.ts +265 -6
- package/src/tools/agent-workspace-tool.ts +305 -0
- package/src/tools/artifact-archive.ts +169 -0
- package/src/tools/tool-definition-compaction.ts +36 -0
- package/src/version.ts +1 -1
|
@@ -0,0 +1,673 @@
|
|
|
1
|
+
import type { CommandContext } from '../input/command-registry.ts';
|
|
2
|
+
import type { AgentHarnessModelRoutingArgs, LocalModelDetection, LocalModelEndpointSource, LocalModelServerDefaultEndpoint, LocalModelServerEndpoint, LocalModelServerHealthMap, LocalModelSmokeTarget, MutableLocalModelServerEndpoint } from './agent-harness-model-routing-types.ts';
|
|
3
|
+
import { listProviderIds, listProviderRegistryProviders, listRegistryModels, modelDisplayName, modelModelId, modelProviderId, modelRegistryKey, readProviderModels } from './agent-harness-model-catalog.ts';
|
|
4
|
+
import { previewHarnessText } from './agent-harness-text.ts';
|
|
5
|
+
import { readLimit, readRecord, readString } from './agent-harness-model-routing-utils.ts';
|
|
6
|
+
|
|
7
|
+
export function localStackFor(value: string): string | null {
|
|
8
|
+
const normalized = value.toLowerCase();
|
|
9
|
+
if (/ollama[-_\s]?cloud/.test(normalized)) return null;
|
|
10
|
+
if (/\bollama\b/.test(normalized)) return 'ollama';
|
|
11
|
+
if (/llama[.-]?cpp|llamacpp/.test(normalized)) return 'llama.cpp';
|
|
12
|
+
if (/\bvllm\b/.test(normalized)) return 'vllm';
|
|
13
|
+
if (/lm[-_\s]?studio/.test(normalized)) return 'openai-compatible';
|
|
14
|
+
if (/localai|text-generation-inference|\btgi\b/.test(normalized)) return 'openai-compatible';
|
|
15
|
+
if (/localhost|127\.0\.0\.1|\[?::1\]?/.test(normalized)) return 'openai-compatible';
|
|
16
|
+
if (/openai-compatible|openai compatible|custom-provider|custom provider/.test(normalized)) return 'openai-compatible';
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function cleanUrlCandidate(value: string): string {
|
|
21
|
+
return value.trim().replace(/[),.;]+$/g, '');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function extractUrls(value: string): readonly string[] {
|
|
25
|
+
const matches = value.match(/https?:\/\/[^\s"'`<>]+/gi) ?? [];
|
|
26
|
+
return [...new Set(matches.map(cleanUrlCandidate).filter(Boolean))];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function parseUrlCandidate(raw: string): URL | null {
|
|
30
|
+
const trimmed = cleanUrlCandidate(raw);
|
|
31
|
+
if (!trimmed) return null;
|
|
32
|
+
const withScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed) ? trimmed : `http://${trimmed}`;
|
|
33
|
+
try {
|
|
34
|
+
return new URL(withScheme);
|
|
35
|
+
} catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function isPrivateOrLocalHost(hostname: string): boolean {
|
|
41
|
+
const host = hostname.replace(/^\[|\]$/g, '').toLowerCase();
|
|
42
|
+
if (!host) return false;
|
|
43
|
+
if (host === 'localhost' || host === '0.0.0.0' || host === '::1') return true;
|
|
44
|
+
if (host.endsWith('.local')) return true;
|
|
45
|
+
if (host.includes(':')) return host.startsWith('fc') || host.startsWith('fd') || host.startsWith('fe80:');
|
|
46
|
+
const octets = host.split('.').map((entry) => Number(entry));
|
|
47
|
+
if (octets.length === 4 && octets.every((entry) => Number.isInteger(entry) && entry >= 0 && entry <= 255)) {
|
|
48
|
+
const [first, second] = octets as [number, number, number, number];
|
|
49
|
+
return first === 10
|
|
50
|
+
|| first === 127
|
|
51
|
+
|| (first === 172 && second >= 16 && second <= 31)
|
|
52
|
+
|| (first === 192 && second === 168)
|
|
53
|
+
|| (first === 169 && second === 254)
|
|
54
|
+
|| first === 0;
|
|
55
|
+
}
|
|
56
|
+
return !host.includes('.');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function isPrivateOrLocalUrl(raw: string): boolean {
|
|
60
|
+
const url = parseUrlCandidate(raw);
|
|
61
|
+
if (!url || !/^https?:$/.test(url.protocol)) return false;
|
|
62
|
+
return isPrivateOrLocalHost(url.hostname);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function normalizeLocalBaseUrl(raw: string, stackHint?: string | null): string | null {
|
|
66
|
+
const url = parseUrlCandidate(raw);
|
|
67
|
+
if (!url || !/^https?:$/.test(url.protocol)) return null;
|
|
68
|
+
const stack = stackHint ?? localStackFor(raw) ?? (isPrivateOrLocalHost(url.hostname) ? 'openai-compatible' : null);
|
|
69
|
+
if (!isPrivateOrLocalUrl(url.href)) return null;
|
|
70
|
+
|
|
71
|
+
let pathname = url.pathname.replace(/\/+$/g, '');
|
|
72
|
+
if (pathname.endsWith('/models')) pathname = pathname.slice(0, -'/models'.length);
|
|
73
|
+
if (pathname.endsWith('/api/tags')) pathname = pathname.slice(0, -'/api/tags'.length);
|
|
74
|
+
const needsOpenAiPath = stack === 'ollama' || stack === 'llama.cpp' || stack === 'vllm' || stack === 'openai-compatible';
|
|
75
|
+
if (needsOpenAiPath && (!pathname || pathname === '/')) pathname = '/v1';
|
|
76
|
+
url.pathname = pathname || '';
|
|
77
|
+
url.search = '';
|
|
78
|
+
url.hash = '';
|
|
79
|
+
return url.toString().replace(/\/+$/g, '');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function modelsUrlFor(baseUrl: string): string {
|
|
83
|
+
return `${baseUrl.replace(/\/+$/g, '')}/models`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function localProviderNameFor(providerId: string | null, stack: string | null, fallback: string): string {
|
|
87
|
+
const seed = providerId || stack || fallback;
|
|
88
|
+
const normalized = seed.toLowerCase().replace(/[^a-z0-9._-]+/g, '-').replace(/^-+|-+$/g, '');
|
|
89
|
+
return normalized || 'local-openai';
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function localProviderAddRoute(providerId: string | null, stack: string | null, baseUrl: string): string {
|
|
93
|
+
const name = localProviderNameFor(providerId, stack, 'local-openai');
|
|
94
|
+
return `agent_harness mode:"run_command" command:"/provider add ${name} ${baseUrl} local --yes" confirm:true explicitUserRequest:"Add this local provider after the server is running."`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function localEndpointId(baseUrl: string): string {
|
|
98
|
+
return `local-${baseUrl.toLowerCase().replace(/^https?:\/\//, '').replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '')}`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function localEndpointInspectRoute(endpointId: string): string {
|
|
102
|
+
return `agent_harness mode:"model_route" modelRouteId:"${endpointId}"`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function localEndpointSmokeRoute(endpointId?: string): string {
|
|
106
|
+
const target = endpointId ? ` modelRouteId:"${endpointId}"` : '';
|
|
107
|
+
return `models action:"smoke"${target} confirm:true explicitUserRequest:"Check local model servers."`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function localEndpointDiagnostics(endpoint: MutableLocalModelServerEndpoint, providerExists: boolean): NonNullable<LocalModelServerEndpoint['diagnostics']> {
|
|
111
|
+
const stack = endpoint.stack ?? 'OpenAI-compatible';
|
|
112
|
+
return {
|
|
113
|
+
liveProbe: 'not-run',
|
|
114
|
+
successCriteria: [
|
|
115
|
+
'The confirmed smoke command exits 0.',
|
|
116
|
+
'The model-list endpoint returns JSON without credentials in output.',
|
|
117
|
+
'At least one model id is visible before refresh or benchmark.',
|
|
118
|
+
],
|
|
119
|
+
failureTriage: [
|
|
120
|
+
`If connection is refused, start the ${stack} server and load a model before refreshing Agent models.`,
|
|
121
|
+
'If the endpoint returns 404, verify the base URL path; most OpenAI-compatible servers should use /v1.',
|
|
122
|
+
'If the host is 0.0.0.0 or a LAN address, switch Agent to a trusted client URL such as 127.0.0.1 or the intended private host.',
|
|
123
|
+
],
|
|
124
|
+
afterSmoke: providerExists
|
|
125
|
+
? ['Run the refresh route, then run a local benchmark before changing the default route.']
|
|
126
|
+
: ['Add the provider route only after smoke succeeds, then refresh models and run a local benchmark.'],
|
|
127
|
+
policy: 'Diagnostics are read-only criteria and confirmed route hints. Agent probes local model-list endpoints only through models action:"smoke" after explicit confirmation; provider add, refresh, benchmark, and route changes remain separate actions.',
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function localModelServerDefaults(): readonly LocalModelServerDefaultEndpoint[] {
|
|
132
|
+
const defaults = [
|
|
133
|
+
{
|
|
134
|
+
id: 'ollama',
|
|
135
|
+
label: 'Ollama',
|
|
136
|
+
stack: 'ollama',
|
|
137
|
+
baseUrl: 'http://127.0.0.1:11434/v1',
|
|
138
|
+
startHint: 'Start Ollama, pull a practical model, then refresh models.',
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
id: 'lm-studio',
|
|
142
|
+
label: 'LM Studio',
|
|
143
|
+
stack: 'openai-compatible',
|
|
144
|
+
baseUrl: 'http://127.0.0.1:1234/v1',
|
|
145
|
+
startHint: 'Start the LM Studio local server and load one model.',
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
id: 'llama-cpp',
|
|
149
|
+
label: 'llama.cpp',
|
|
150
|
+
stack: 'llama.cpp',
|
|
151
|
+
baseUrl: 'http://127.0.0.1:8080/v1',
|
|
152
|
+
startHint: 'Run llama-server with a GGUF model before adding the provider.',
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
id: 'vllm',
|
|
156
|
+
label: 'vLLM',
|
|
157
|
+
stack: 'vllm',
|
|
158
|
+
baseUrl: 'http://127.0.0.1:8000/v1',
|
|
159
|
+
startHint: 'Start the vLLM OpenAI-compatible API server after GPU/driver checks.',
|
|
160
|
+
},
|
|
161
|
+
] as const;
|
|
162
|
+
return defaults.map((entry) => ({
|
|
163
|
+
...entry,
|
|
164
|
+
modelsUrl: modelsUrlFor(entry.baseUrl),
|
|
165
|
+
smokeCommand: `curl -fsS ${modelsUrlFor(entry.baseUrl)}`,
|
|
166
|
+
addProviderRoute: localProviderAddRoute(null, entry.stack, entry.baseUrl),
|
|
167
|
+
}));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function localModelDetection(context: CommandContext): LocalModelDetection {
|
|
171
|
+
const providerIds = new Set<string>();
|
|
172
|
+
const modelRoutes = new Set<string>();
|
|
173
|
+
const stacks = new Set<string>();
|
|
174
|
+
for (const providerId of listProviderIds(context)) {
|
|
175
|
+
const stack = localStackFor(providerId);
|
|
176
|
+
if (!stack) continue;
|
|
177
|
+
providerIds.add(providerId);
|
|
178
|
+
stacks.add(stack);
|
|
179
|
+
}
|
|
180
|
+
for (const model of listRegistryModels(context)) {
|
|
181
|
+
const record = readRecord(model);
|
|
182
|
+
const providerId = modelProviderId(model);
|
|
183
|
+
const registryKey = modelRegistryKey(model);
|
|
184
|
+
const fields = [
|
|
185
|
+
providerId,
|
|
186
|
+
registryKey,
|
|
187
|
+
modelModelId(model),
|
|
188
|
+
modelDisplayName(model),
|
|
189
|
+
readString(record.description),
|
|
190
|
+
readString(record.baseURL),
|
|
191
|
+
readString(record.baseUrl),
|
|
192
|
+
readString(record.serverType),
|
|
193
|
+
JSON.stringify(record.providerEnvVars ?? ''),
|
|
194
|
+
].join('\n');
|
|
195
|
+
const stack = localStackFor(fields);
|
|
196
|
+
if (!stack) continue;
|
|
197
|
+
if (providerId) providerIds.add(providerId);
|
|
198
|
+
if (registryKey) modelRoutes.add(registryKey);
|
|
199
|
+
stacks.add(stack);
|
|
200
|
+
}
|
|
201
|
+
return {
|
|
202
|
+
providerIds: [...providerIds].sort((a, b) => a.localeCompare(b)),
|
|
203
|
+
modelRoutes: [...modelRoutes].sort((a, b) => a.localeCompare(b)),
|
|
204
|
+
stacks: [...stacks].sort((a, b) => a.localeCompare(b)),
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function addLocalServerEndpoint(
|
|
209
|
+
endpoints: Map<string, MutableLocalModelServerEndpoint>,
|
|
210
|
+
input: {
|
|
211
|
+
readonly baseUrl: string;
|
|
212
|
+
readonly providerId?: string | null;
|
|
213
|
+
readonly stack?: string | null;
|
|
214
|
+
readonly source: LocalModelEndpointSource;
|
|
215
|
+
readonly sourceDetail: string;
|
|
216
|
+
readonly modelRoutes?: readonly string[];
|
|
217
|
+
readonly notes?: readonly string[];
|
|
218
|
+
},
|
|
219
|
+
): void {
|
|
220
|
+
const normalized = normalizeLocalBaseUrl(input.baseUrl, input.stack);
|
|
221
|
+
if (!normalized) return;
|
|
222
|
+
const stack = input.stack ?? localStackFor(input.baseUrl) ?? localStackFor(`${input.providerId ?? ''}\n${input.sourceDetail}`) ?? 'openai-compatible';
|
|
223
|
+
const existing = endpoints.get(normalized);
|
|
224
|
+
if (existing) {
|
|
225
|
+
if (!existing.providerId && input.providerId) existing.providerId = input.providerId;
|
|
226
|
+
if (!existing.stack && stack) existing.stack = stack;
|
|
227
|
+
existing.sources.add(input.source);
|
|
228
|
+
existing.sourceDetails.add(input.sourceDetail);
|
|
229
|
+
for (const route of input.modelRoutes ?? []) existing.modelRoutes.add(route);
|
|
230
|
+
for (const note of input.notes ?? []) existing.notes.add(note);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
endpoints.set(normalized, {
|
|
234
|
+
providerId: input.providerId ?? null,
|
|
235
|
+
stack,
|
|
236
|
+
baseUrl: normalized,
|
|
237
|
+
sources: new Set([input.source]),
|
|
238
|
+
sourceDetails: new Set([input.sourceDetail]),
|
|
239
|
+
modelRoutes: new Set(input.modelRoutes ?? []),
|
|
240
|
+
notes: new Set(input.notes ?? []),
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export function collectLocalServerEndpointCandidates(context: CommandContext): readonly MutableLocalModelServerEndpoint[] {
|
|
245
|
+
const endpoints = new Map<string, MutableLocalModelServerEndpoint>();
|
|
246
|
+
for (const provider of listProviderRegistryProviders(context)) {
|
|
247
|
+
const record = readRecord(provider);
|
|
248
|
+
const providerId = readString(record.name) || readString(record.id) || readString(record.providerId) || readString(record.provider);
|
|
249
|
+
const fields = [
|
|
250
|
+
providerId,
|
|
251
|
+
readString(record.label),
|
|
252
|
+
readString(record.displayName),
|
|
253
|
+
readString(record.description),
|
|
254
|
+
readString(record.baseURL),
|
|
255
|
+
readString(record.baseUrl),
|
|
256
|
+
readString(record.endpoint),
|
|
257
|
+
JSON.stringify(record.serviceNames ?? ''),
|
|
258
|
+
JSON.stringify(record.envVars ?? ''),
|
|
259
|
+
].join('\n');
|
|
260
|
+
const stack = localStackFor(fields);
|
|
261
|
+
const routes = readProviderModels(record.models, providerId);
|
|
262
|
+
const urls = [
|
|
263
|
+
readString(record.baseURL),
|
|
264
|
+
readString(record.baseUrl),
|
|
265
|
+
readString(record.endpoint),
|
|
266
|
+
...extractUrls(fields),
|
|
267
|
+
].filter(Boolean);
|
|
268
|
+
for (const baseUrl of urls) {
|
|
269
|
+
addLocalServerEndpoint(endpoints, {
|
|
270
|
+
baseUrl,
|
|
271
|
+
providerId: providerId || null,
|
|
272
|
+
stack: stack ?? localStackFor(baseUrl),
|
|
273
|
+
source: 'provider-registry',
|
|
274
|
+
sourceDetail: providerId ? `provider:${providerId}` : 'provider-registry',
|
|
275
|
+
modelRoutes: routes,
|
|
276
|
+
notes: providerId ? ['Provider already exists in the registry.'] : [],
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
for (const model of listRegistryModels(context)) {
|
|
282
|
+
const record = readRecord(model);
|
|
283
|
+
const providerId = modelProviderId(model);
|
|
284
|
+
const registryKey = modelRegistryKey(model);
|
|
285
|
+
const fields = [
|
|
286
|
+
providerId,
|
|
287
|
+
registryKey,
|
|
288
|
+
modelModelId(model),
|
|
289
|
+
modelDisplayName(model),
|
|
290
|
+
readString(record.description),
|
|
291
|
+
readString(record.baseURL),
|
|
292
|
+
readString(record.baseUrl),
|
|
293
|
+
readString(record.serverType),
|
|
294
|
+
JSON.stringify(record.providerEnvVars ?? ''),
|
|
295
|
+
].join('\n');
|
|
296
|
+
const stack = localStackFor(fields);
|
|
297
|
+
const urls = [
|
|
298
|
+
readString(record.baseURL),
|
|
299
|
+
readString(record.baseUrl),
|
|
300
|
+
readString(record.endpoint),
|
|
301
|
+
...extractUrls(readString(record.description)),
|
|
302
|
+
].filter(Boolean);
|
|
303
|
+
for (const baseUrl of urls) {
|
|
304
|
+
addLocalServerEndpoint(endpoints, {
|
|
305
|
+
baseUrl,
|
|
306
|
+
providerId: providerId || null,
|
|
307
|
+
stack: stack ?? localStackFor(baseUrl),
|
|
308
|
+
source: 'model-registry',
|
|
309
|
+
sourceDetail: registryKey ? `model:${registryKey}` : 'model-registry',
|
|
310
|
+
modelRoutes: registryKey ? [registryKey] : [],
|
|
311
|
+
notes: registryKey ? ['At least one model route is already registered for this endpoint.'] : [],
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const envHints: readonly { readonly key: string; readonly stack: string; readonly note: string }[] = [
|
|
317
|
+
{ key: 'OLLAMA_BASE_URL', stack: 'ollama', note: 'Environment override for the Ollama base URL.' },
|
|
318
|
+
{ key: 'OLLAMA_HOST', stack: 'ollama', note: 'Environment override for the Ollama host.' },
|
|
319
|
+
{ key: 'LM_STUDIO_BASE_URL', stack: 'openai-compatible', note: 'Environment override for LM Studio.' },
|
|
320
|
+
{ key: 'OPENAI_COMPATIBLE_BASE_URL', stack: 'openai-compatible', note: 'Environment override for a local OpenAI-compatible server.' },
|
|
321
|
+
{ key: 'OPENAI_COMPAT_BASE_URL', stack: 'openai-compatible', note: 'Environment override for a local OpenAI-compatible server.' },
|
|
322
|
+
{ key: 'VLLM_BASE_URL', stack: 'vllm', note: 'Environment override for vLLM.' },
|
|
323
|
+
{ key: 'LLAMA_CPP_BASE_URL', stack: 'llama.cpp', note: 'Environment override for llama.cpp.' },
|
|
324
|
+
{ key: 'LITELLM_BASE_URL', stack: 'openai-compatible', note: 'Environment override for LiteLLM or a local gateway.' },
|
|
325
|
+
];
|
|
326
|
+
for (const hint of envHints) {
|
|
327
|
+
const value = readString(process.env[hint.key]);
|
|
328
|
+
if (!value) continue;
|
|
329
|
+
addLocalServerEndpoint(endpoints, {
|
|
330
|
+
baseUrl: value,
|
|
331
|
+
stack: hint.stack,
|
|
332
|
+
source: 'environment',
|
|
333
|
+
sourceDetail: `env:${hint.key}`,
|
|
334
|
+
notes: [hint.note],
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return [...endpoints.values()].sort((left, right) => left.baseUrl.localeCompare(right.baseUrl));
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
export function describeLocalServerEndpoint(endpoint: MutableLocalModelServerEndpoint, includeParameters = false): LocalModelServerEndpoint {
|
|
342
|
+
const modelsUrl = modelsUrlFor(endpoint.baseUrl);
|
|
343
|
+
const providerExists = Boolean(endpoint.providerId) || endpoint.modelRoutes.size > 0;
|
|
344
|
+
const notes = new Set(endpoint.notes);
|
|
345
|
+
if (endpoint.baseUrl.includes('0.0.0.0')) {
|
|
346
|
+
notes.add('0.0.0.0 is usually a listen address; prefer 127.0.0.1 or a trusted LAN host for the client provider URL.');
|
|
347
|
+
}
|
|
348
|
+
if (!providerExists) {
|
|
349
|
+
notes.add('No registered model route was found for this endpoint yet.');
|
|
350
|
+
}
|
|
351
|
+
const id = localEndpointId(endpoint.baseUrl);
|
|
352
|
+
return {
|
|
353
|
+
kind: 'local-server-endpoint',
|
|
354
|
+
id,
|
|
355
|
+
providerId: endpoint.providerId,
|
|
356
|
+
stack: endpoint.stack,
|
|
357
|
+
baseUrl: endpoint.baseUrl,
|
|
358
|
+
modelsUrl,
|
|
359
|
+
diagnosticStatus: providerExists ? 'registered-route-needs-smoke' : 'needs-provider-after-smoke',
|
|
360
|
+
inspectRoute: localEndpointInspectRoute(id),
|
|
361
|
+
sources: [...endpoint.sources].sort((a, b) => a.localeCompare(b)),
|
|
362
|
+
sourceDetails: [...endpoint.sourceDetails].sort((a, b) => a.localeCompare(b)),
|
|
363
|
+
modelRoutes: [...endpoint.modelRoutes].sort((a, b) => a.localeCompare(b)),
|
|
364
|
+
smokeCommand: `curl -fsS ${modelsUrl}`,
|
|
365
|
+
smokeRoute: localEndpointSmokeRoute(id),
|
|
366
|
+
refreshRoute: 'agent_harness mode:"run_command" command:"/refresh-models" confirm:true explicitUserRequest:"Refresh models after verifying the local server."',
|
|
367
|
+
addProviderRoute: providerExists ? null : localProviderAddRoute(endpoint.providerId, endpoint.stack, endpoint.baseUrl),
|
|
368
|
+
notes: [...notes],
|
|
369
|
+
...(includeParameters ? { diagnostics: localEndpointDiagnostics(endpoint, providerExists) } : {}),
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
export function localModelServerHealthMap(
|
|
374
|
+
context: CommandContext,
|
|
375
|
+
includeParameters: boolean,
|
|
376
|
+
): LocalModelServerHealthMap {
|
|
377
|
+
const endpoints = collectLocalServerEndpointCandidates(context).map((endpoint) => describeLocalServerEndpoint(endpoint, includeParameters));
|
|
378
|
+
const returned = endpoints.slice(0, includeParameters ? 8 : 3);
|
|
379
|
+
const suggestedDefaults = localModelServerDefaults().slice(0, includeParameters ? 4 : 2);
|
|
380
|
+
const first = returned[0];
|
|
381
|
+
return {
|
|
382
|
+
status: endpoints.length > 0 ? 'candidate-endpoints' : 'no-local-endpoints',
|
|
383
|
+
liveProbe: 'not-run',
|
|
384
|
+
endpointCount: endpoints.length,
|
|
385
|
+
returnedEndpoints: returned.length,
|
|
386
|
+
endpoints: returned,
|
|
387
|
+
suggestedDefaults,
|
|
388
|
+
nextActions: endpoints.length > 0
|
|
389
|
+
? [
|
|
390
|
+
`Smoke test ${first?.modelsUrl ?? 'the detected model-list endpoint'} before benchmark or route changes.`,
|
|
391
|
+
'Refresh the model catalog after the local server is running and reachable.',
|
|
392
|
+
'Run the local benchmark comparison before making a local route the default.',
|
|
393
|
+
]
|
|
394
|
+
: [
|
|
395
|
+
'Start one local server from suggestedDefaults, then smoke test its model-list endpoint.',
|
|
396
|
+
'Add or select the provider route only after the server is reachable.',
|
|
397
|
+
'Refresh models and run the local benchmark before changing the default route.',
|
|
398
|
+
],
|
|
399
|
+
policy: 'Read-only local endpoint map. It derives candidate model-list URLs, smoke commands, and confirmed route hints from registry/env metadata; it does not probe the network, install servers, download models, add providers, refresh models, benchmark, or change routes.',
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function localModelSmokeTargetFromEndpoint(endpoint: LocalModelServerEndpoint): LocalModelSmokeTarget {
|
|
404
|
+
return {
|
|
405
|
+
kind: endpoint.kind,
|
|
406
|
+
id: endpoint.id,
|
|
407
|
+
label: `Local model server ${endpoint.baseUrl}`,
|
|
408
|
+
providerId: endpoint.providerId,
|
|
409
|
+
stack: endpoint.stack,
|
|
410
|
+
baseUrl: endpoint.baseUrl,
|
|
411
|
+
modelsUrl: endpoint.modelsUrl,
|
|
412
|
+
smokeCommand: endpoint.smokeCommand,
|
|
413
|
+
smokeRoute: endpoint.smokeRoute,
|
|
414
|
+
refreshRoute: endpoint.refreshRoute,
|
|
415
|
+
addProviderRoute: endpoint.addProviderRoute,
|
|
416
|
+
source: endpoint.sources.join(', ') || 'local-endpoint',
|
|
417
|
+
notes: endpoint.notes,
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function localModelSmokeTargetFromDefault(endpoint: LocalModelServerDefaultEndpoint): LocalModelSmokeTarget {
|
|
422
|
+
return {
|
|
423
|
+
kind: 'suggested-local-server',
|
|
424
|
+
id: endpoint.id,
|
|
425
|
+
label: endpoint.label,
|
|
426
|
+
providerId: null,
|
|
427
|
+
stack: endpoint.stack,
|
|
428
|
+
baseUrl: endpoint.baseUrl,
|
|
429
|
+
modelsUrl: endpoint.modelsUrl,
|
|
430
|
+
smokeCommand: endpoint.smokeCommand,
|
|
431
|
+
smokeRoute: localEndpointSmokeRoute(endpoint.id),
|
|
432
|
+
refreshRoute: 'agent_harness mode:"run_command" command:"/refresh-models" confirm:true explicitUserRequest:"Refresh models after verifying the local server."',
|
|
433
|
+
addProviderRoute: endpoint.addProviderRoute,
|
|
434
|
+
source: 'suggested-default',
|
|
435
|
+
notes: [endpoint.startHint],
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function localSmokeTargetSearchText(target: LocalModelSmokeTarget): string {
|
|
440
|
+
return [
|
|
441
|
+
target.kind,
|
|
442
|
+
target.id,
|
|
443
|
+
target.label,
|
|
444
|
+
target.providerId ?? '',
|
|
445
|
+
target.stack ?? '',
|
|
446
|
+
target.baseUrl,
|
|
447
|
+
target.modelsUrl,
|
|
448
|
+
target.source,
|
|
449
|
+
...target.notes,
|
|
450
|
+
].join('\n').toLowerCase();
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function localModelSmokeLookup(args: AgentHarnessModelRoutingArgs): string {
|
|
454
|
+
const fields = readRecord(args.fields);
|
|
455
|
+
return readString(args.modelRouteId)
|
|
456
|
+
|| readString(args.target)
|
|
457
|
+
|| readString(args.query)
|
|
458
|
+
|| readString(fields.endpointId)
|
|
459
|
+
|| readString(fields.modelRouteId)
|
|
460
|
+
|| readString(fields.baseUrl)
|
|
461
|
+
|| readString(fields.modelsUrl);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function localModelSmokeTargets(context: CommandContext, args: AgentHarnessModelRoutingArgs): Record<string, unknown> | readonly LocalModelSmokeTarget[] {
|
|
465
|
+
const endpoints = collectLocalServerEndpointCandidates(context)
|
|
466
|
+
.map((endpoint) => localModelSmokeTargetFromEndpoint(describeLocalServerEndpoint(endpoint, true)));
|
|
467
|
+
const defaults = localModelServerDefaults().map(localModelSmokeTargetFromDefault);
|
|
468
|
+
const lookup = localModelSmokeLookup(args);
|
|
469
|
+
const allTargets = [...endpoints, ...defaults];
|
|
470
|
+
if (lookup) {
|
|
471
|
+
const normalized = lookup.toLowerCase();
|
|
472
|
+
const exact = allTargets.filter((target) => target.id === lookup || target.baseUrl === lookup || target.modelsUrl === lookup);
|
|
473
|
+
if (exact.length === 1) return exact;
|
|
474
|
+
if (exact.length > 1) {
|
|
475
|
+
return {
|
|
476
|
+
status: 'ambiguous',
|
|
477
|
+
input: lookup,
|
|
478
|
+
candidates: exact.slice(0, 8).map((target) => ({
|
|
479
|
+
kind: target.kind,
|
|
480
|
+
id: target.id,
|
|
481
|
+
label: target.label,
|
|
482
|
+
baseUrl: target.baseUrl,
|
|
483
|
+
modelsUrl: target.modelsUrl,
|
|
484
|
+
})),
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
const searched = allTargets.filter((target) => localSmokeTargetSearchText(target).includes(normalized));
|
|
488
|
+
if (searched.length === 1) return searched;
|
|
489
|
+
if (searched.length > 1) {
|
|
490
|
+
return {
|
|
491
|
+
status: 'ambiguous',
|
|
492
|
+
input: lookup,
|
|
493
|
+
candidates: searched.slice(0, 8).map((target) => ({
|
|
494
|
+
kind: target.kind,
|
|
495
|
+
id: target.id,
|
|
496
|
+
label: target.label,
|
|
497
|
+
baseUrl: target.baseUrl,
|
|
498
|
+
modelsUrl: target.modelsUrl,
|
|
499
|
+
})),
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
return {
|
|
503
|
+
status: 'missing_lookup',
|
|
504
|
+
input: lookup,
|
|
505
|
+
usage: 'Unknown local model endpoint. Use models action:"local" includeParameters:true to inspect local endpoint ids, or omit the lookup to check detected/default local servers.',
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
const pool = endpoints.length ? endpoints : defaults;
|
|
509
|
+
return pool.slice(0, readLimit(args.limit, 4));
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function readSmokeTimeoutMs(value: unknown): number {
|
|
513
|
+
const parsed = typeof value === 'string' && value.trim() ? Number(value) : value;
|
|
514
|
+
if (typeof parsed !== 'number' || !Number.isFinite(parsed)) return 1500;
|
|
515
|
+
return Math.max(250, Math.min(10000, Math.trunc(parsed)));
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function localSmokeNetworkScope(modelsUrl: string): { readonly allowed: boolean; readonly scope: string; readonly reason?: string } {
|
|
519
|
+
const url = parseUrlCandidate(modelsUrl);
|
|
520
|
+
if (!url || !/^https?:$/.test(url.protocol)) return { allowed: false, scope: 'invalid-url', reason: 'The model-list URL is not a valid HTTP(S) URL.' };
|
|
521
|
+
const host = url.hostname.replace(/^\[|\]$/g, '').toLowerCase();
|
|
522
|
+
if (host === '0.0.0.0') {
|
|
523
|
+
return { allowed: false, scope: 'bind-all-host', reason: '0.0.0.0 is a bind address, not a client URL. Use 127.0.0.1 or the intended LAN host.' };
|
|
524
|
+
}
|
|
525
|
+
if (!isPrivateOrLocalUrl(url.href)) {
|
|
526
|
+
return { allowed: false, scope: 'non-local-host', reason: 'Local model smoke only probes loopback, local-name, or private LAN endpoints.' };
|
|
527
|
+
}
|
|
528
|
+
if (host === 'localhost' || host === '127.0.0.1' || host === '::1') return { allowed: true, scope: 'loopback' };
|
|
529
|
+
if (host.endsWith('.local') || !host.includes('.')) return { allowed: true, scope: 'local-name' };
|
|
530
|
+
return { allowed: true, scope: 'private-lan' };
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function extractModelIdsFromPayload(payload: unknown): readonly string[] {
|
|
534
|
+
const record = readRecord(payload);
|
|
535
|
+
const candidates = Array.isArray(record.data)
|
|
536
|
+
? record.data
|
|
537
|
+
: Array.isArray(record.models)
|
|
538
|
+
? record.models
|
|
539
|
+
: Array.isArray(payload)
|
|
540
|
+
? payload
|
|
541
|
+
: [];
|
|
542
|
+
const ids = candidates.map((entry) => {
|
|
543
|
+
if (typeof entry === 'string') return entry;
|
|
544
|
+
const item = readRecord(entry);
|
|
545
|
+
return readString(item.id) || readString(item.name) || readString(item.model);
|
|
546
|
+
}).filter(Boolean);
|
|
547
|
+
return [...new Set(ids)].slice(0, 12);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function safeSmokeError(error: unknown): string {
|
|
551
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
552
|
+
return previewHarnessText(message.replace(/https?:\/\/\S+/g, '[redacted-url]'), 180);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
async function smokeOneLocalModelTarget(target: LocalModelSmokeTarget, timeoutMs: number): Promise<Record<string, unknown>> {
|
|
556
|
+
const network = localSmokeNetworkScope(target.modelsUrl);
|
|
557
|
+
if (!network.allowed) {
|
|
558
|
+
return {
|
|
559
|
+
...target,
|
|
560
|
+
status: 'blocked',
|
|
561
|
+
liveProbe: 'confirmed',
|
|
562
|
+
networkScope: network.scope,
|
|
563
|
+
failure: network.reason,
|
|
564
|
+
nextActions: ['Inspect the endpoint route and correct the base URL before running smoke again.'],
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
const controller = new AbortController();
|
|
569
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
570
|
+
const started = Date.now();
|
|
571
|
+
try {
|
|
572
|
+
const response = await fetch(target.modelsUrl, {
|
|
573
|
+
method: 'GET',
|
|
574
|
+
headers: { Accept: 'application/json' },
|
|
575
|
+
signal: controller.signal,
|
|
576
|
+
});
|
|
577
|
+
const elapsedMs = Date.now() - started;
|
|
578
|
+
const contentType = response.headers.get('content-type') ?? '';
|
|
579
|
+
const text = await response.text();
|
|
580
|
+
let payload: unknown = null;
|
|
581
|
+
let jsonValid = false;
|
|
582
|
+
try {
|
|
583
|
+
payload = text ? JSON.parse(text) : null;
|
|
584
|
+
jsonValid = true;
|
|
585
|
+
} catch {
|
|
586
|
+
jsonValid = false;
|
|
587
|
+
}
|
|
588
|
+
const modelIds = jsonValid ? extractModelIdsFromPayload(payload) : [];
|
|
589
|
+
const status = !response.ok
|
|
590
|
+
? 'http-error'
|
|
591
|
+
: !jsonValid
|
|
592
|
+
? 'invalid-json'
|
|
593
|
+
: modelIds.length === 0
|
|
594
|
+
? 'no-models'
|
|
595
|
+
: 'passed';
|
|
596
|
+
return {
|
|
597
|
+
...target,
|
|
598
|
+
status,
|
|
599
|
+
liveProbe: 'confirmed',
|
|
600
|
+
networkScope: network.scope,
|
|
601
|
+
httpStatus: response.status,
|
|
602
|
+
contentType,
|
|
603
|
+
elapsedMs,
|
|
604
|
+
jsonValid,
|
|
605
|
+
modelCount: modelIds.length,
|
|
606
|
+
sampleModelIds: modelIds.slice(0, 5),
|
|
607
|
+
success: status === 'passed',
|
|
608
|
+
nextActions: status === 'passed'
|
|
609
|
+
? ['Refresh the model catalog, then run a local benchmark before changing the default model.']
|
|
610
|
+
: ['Start or fix the local server, confirm /v1/models returns model ids, then retry this smoke check.'],
|
|
611
|
+
};
|
|
612
|
+
} catch (error) {
|
|
613
|
+
const elapsedMs = Date.now() - started;
|
|
614
|
+
const aborted = controller.signal.aborted;
|
|
615
|
+
return {
|
|
616
|
+
...target,
|
|
617
|
+
status: aborted ? 'timeout' : 'unreachable',
|
|
618
|
+
liveProbe: 'confirmed',
|
|
619
|
+
networkScope: network.scope,
|
|
620
|
+
elapsedMs,
|
|
621
|
+
timeoutMs,
|
|
622
|
+
success: false,
|
|
623
|
+
failure: aborted ? `Timed out after ${timeoutMs}ms.` : safeSmokeError(error),
|
|
624
|
+
nextActions: ['Start the local server, load at least one model, verify the base URL, then retry this smoke check.'],
|
|
625
|
+
};
|
|
626
|
+
} finally {
|
|
627
|
+
clearTimeout(timeout);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
export async function runLocalModelServerSmoke(context: CommandContext, args: AgentHarnessModelRoutingArgs): Promise<Record<string, unknown>> {
|
|
632
|
+
const targets = localModelSmokeTargets(context, args);
|
|
633
|
+
if (!Array.isArray(targets)) {
|
|
634
|
+
return {
|
|
635
|
+
kind: 'local-model-smoke',
|
|
636
|
+
liveProbe: 'not-run',
|
|
637
|
+
...targets,
|
|
638
|
+
policy: 'No local model endpoint was probed because the requested endpoint lookup did not resolve exactly.',
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
if (targets.length === 0) {
|
|
642
|
+
return {
|
|
643
|
+
kind: 'local-model-smoke',
|
|
644
|
+
status: 'no-candidates',
|
|
645
|
+
liveProbe: 'not-run',
|
|
646
|
+
endpoints: [],
|
|
647
|
+
nextActions: ['Use the local model cookbook to start a local server or configure a local provider endpoint.'],
|
|
648
|
+
cookbookRoute: 'models action:"local" includeParameters:true',
|
|
649
|
+
policy: 'No local model endpoint was probed because no candidate endpoints were available.',
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
const timeoutMs = readSmokeTimeoutMs(args.timeoutMs);
|
|
653
|
+
const checkedAt = new Date().toISOString();
|
|
654
|
+
const results = await Promise.all(targets.map((target) => smokeOneLocalModelTarget(target, timeoutMs)));
|
|
655
|
+
const passed = results.filter((result) => result.success === true);
|
|
656
|
+
const blocked = results.filter((result) => result.status === 'blocked');
|
|
657
|
+
return {
|
|
658
|
+
kind: 'local-model-smoke',
|
|
659
|
+
status: passed.length > 0 ? 'ready' : blocked.length === results.length ? 'blocked' : 'needs-attention',
|
|
660
|
+
liveProbe: 'confirmed',
|
|
661
|
+
checkedAt,
|
|
662
|
+
timeoutMs,
|
|
663
|
+
endpointCount: results.length,
|
|
664
|
+
passedCount: passed.length,
|
|
665
|
+
failedCount: results.length - passed.length,
|
|
666
|
+
endpoints: results,
|
|
667
|
+
nextActions: passed.length > 0
|
|
668
|
+
? ['Refresh the model catalog and run the local benchmark action before changing the default route.']
|
|
669
|
+
: ['Start a local model server, load one model, and rerun this confirmed smoke check.'],
|
|
670
|
+
cookbookRoute: 'models action:"local" includeParameters:true',
|
|
671
|
+
policy: 'Confirmed read-only local model smoke. Agent only sends bounded GET requests to discovered or suggested local/private model-list endpoints; it does not add providers, refresh catalogs, benchmark, download models, or change routes.',
|
|
672
|
+
};
|
|
673
|
+
}
|