@jini-ai/agent-runtime 0.4.1 → 0.4.3
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 +8 -0
- package/dist/acp-model-probe.d.ts +6 -27
- package/dist/acp-model-probe.js +6 -4
- package/dist/agent-protocol/acp/models.d.ts +8 -0
- package/dist/agent-protocol/acp/models.js +38 -6
- package/dist/agent-protocol/acp/session.js +15 -2
- package/dist/agent-protocol/pi-rpc/events.js +4 -0
- package/dist/amr-model-cache.d.ts +2 -12
- package/dist/amr-profile-resolver.d.ts +3 -14
- package/dist/anthropic-live-models.d.ts +1 -1
- package/dist/anthropic-live-models.js +0 -0
- package/dist/artifact-taxonomy.d.ts +3 -17
- package/dist/auth.d.ts +0 -6
- package/dist/auth.js +1 -30
- package/dist/claude-code-models.d.ts +3 -4
- package/dist/claude-code-models.js +5 -7
- package/dist/claude-stream.d.ts +0 -4
- package/dist/claude-stream.js +4 -4
- package/dist/copilot-stream.d.ts +0 -5
- package/dist/copilot-stream.js +0 -5
- package/dist/defs/aider.d.ts +15 -0
- package/dist/defs/aider.js +4 -2
- package/dist/defs/amp.d.ts +16 -0
- package/dist/defs/amp.js +5 -1
- package/dist/defs/amr.d.ts +15 -0
- package/dist/defs/amr.js +6 -15
- package/dist/defs/antigravity.d.ts +16 -1
- package/dist/defs/antigravity.js +3 -2
- package/dist/defs/auggie.d.ts +15 -0
- package/dist/defs/auggie.js +3 -0
- package/dist/defs/claude.d.ts +15 -0
- package/dist/defs/claude.js +26 -36
- package/dist/defs/cline.d.ts +15 -0
- package/dist/defs/cline.js +3 -0
- package/dist/defs/codebuddy.d.ts +15 -0
- package/dist/defs/codebuddy.js +3 -1
- package/dist/defs/codex.d.ts +15 -0
- package/dist/defs/codex.js +11 -48
- package/dist/defs/copilot.d.ts +15 -0
- package/dist/defs/copilot.js +5 -14
- package/dist/defs/crush.d.ts +15 -0
- package/dist/defs/crush.js +3 -0
- package/dist/defs/cursor-agent.d.ts +15 -0
- package/dist/defs/cursor-agent.js +3 -1
- package/dist/defs/deepseek.d.ts +15 -0
- package/dist/defs/deepseek.js +3 -1
- package/dist/defs/devin.d.ts +15 -0
- package/dist/defs/devin.js +3 -1
- package/dist/defs/droid.d.ts +15 -0
- package/dist/defs/droid.js +3 -0
- package/dist/defs/goose.d.ts +15 -0
- package/dist/defs/goose.js +3 -0
- package/dist/defs/grok-build.d.ts +15 -0
- package/dist/defs/grok-build.js +5 -9
- package/dist/defs/hermes.d.ts +15 -0
- package/dist/defs/hermes.js +3 -1
- package/dist/defs/index.d.ts +1 -6
- package/dist/defs/index.js +1 -6
- package/dist/defs/kilo.d.ts +15 -0
- package/dist/defs/kilo.js +3 -1
- package/dist/defs/kimi.d.ts +15 -0
- package/dist/defs/kimi.js +3 -1
- package/dist/defs/kiro.d.ts +15 -0
- package/dist/defs/kiro.js +3 -1
- package/dist/defs/mimo.d.ts +15 -0
- package/dist/defs/mimo.js +3 -1
- package/dist/defs/opencode.d.ts +15 -1
- package/dist/defs/opencode.js +2 -1
- package/dist/defs/pi.d.ts +15 -0
- package/dist/defs/pi.js +6 -5
- package/dist/defs/qoder.d.ts +15 -0
- package/dist/defs/qoder.js +3 -1
- package/dist/defs/qwen.d.ts +15 -0
- package/dist/defs/qwen.js +3 -1
- package/dist/defs/reasonix.d.ts +15 -0
- package/dist/defs/reasonix.js +7 -23
- package/dist/defs/shared.d.ts +3 -8
- package/dist/defs/shared.js +3 -8
- package/dist/defs/trae-cli.d.ts +15 -0
- package/dist/defs/trae-cli.js +3 -1
- package/dist/defs/vibe.d.ts +15 -0
- package/dist/defs/vibe.js +3 -1
- package/dist/detection.d.ts +23 -4
- package/dist/detection.js +41 -59
- package/dist/diagnostics.js +1 -9
- package/dist/env.js +5 -31
- package/dist/executables.js +0 -1
- package/dist/index.d.ts +10 -257
- package/dist/index.js +20 -278
- package/dist/json-event-stream.js +23 -29
- package/dist/launch.js +1 -5
- package/dist/log-file.js +0 -5
- package/dist/metadata.js +0 -4
- package/dist/mmd-routes.js +2 -6
- package/dist/model-discovery-adapters.d.ts +22 -0
- package/dist/model-discovery-adapters.js +391 -0
- package/dist/model-discovery-config.d.ts +17 -0
- package/dist/model-discovery-config.js +137 -0
- package/dist/model-discovery-sdk.d.ts +18 -0
- package/dist/model-discovery-sdk.js +56 -0
- package/dist/model-discovery-types.d.ts +139 -0
- package/dist/model-discovery-types.js +2 -0
- package/dist/model-discovery.d.ts +46 -0
- package/dist/model-discovery.js +258 -0
- package/dist/model-registry.d.ts +5 -36
- package/dist/models.d.ts +1 -4
- package/dist/models.js +3 -1
- package/dist/opencode-log.js +1 -3
- package/dist/paths.js +1 -4
- package/dist/pi-models.d.ts +1 -7
- package/dist/pi-models.js +1 -3
- package/dist/prompt-augmenter.d.ts +3 -17
- package/dist/prompt-augmenter.js +3 -17
- package/dist/providers/model-catalog.d.ts +2 -0
- package/dist/providers/model-catalog.js +76 -55
- package/dist/providers/sse-decode.d.ts +4 -48
- package/dist/providers/sse-decode.js +3 -108
- package/dist/qoder-stream.d.ts +0 -3
- package/dist/qoder-stream.js +0 -3
- package/dist/registry.d.ts +1 -1
- package/dist/registry.js +3 -3
- package/dist/resolution.js +1 -4
- package/dist/role-marker-guard.d.ts +0 -5
- package/dist/role-marker-guard.js +0 -5
- package/dist/telemetry-sink.d.ts +3 -14
- package/dist/types.d.ts +9 -2
- package/package.json +27 -6
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { readFile, stat } from 'node:fs/promises';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { join, dirname } from 'node:path';
|
|
5
|
+
import { createCommandInvocation } from '@jini-ai/platform';
|
|
6
|
+
import { detectAcpModelMetadata } from './agent-protocol/acp/models.js';
|
|
7
|
+
import { CLAUDE_CODE_INITIALIZE_ARGS, CLAUDE_CODE_INITIALIZE_REQUEST } from './claude-code-models.js';
|
|
8
|
+
import { listProviderModels } from './providers/model-catalog.js';
|
|
9
|
+
import { execAgentFile } from './invocation.js';
|
|
10
|
+
import { probeCopilotSdk, probeDroidSdk } from './model-discovery-sdk.js';
|
|
11
|
+
import { parseSettingsJson, readModelConfiguration } from './model-discovery-config.js';
|
|
12
|
+
import { ModelProbeError, modelIdentityKind, withModelProbeTimeout } from './model-discovery.js';
|
|
13
|
+
const ACP_ARGS = {
|
|
14
|
+
amr: ['agent', 'run', '--runtime', 'opencode'],
|
|
15
|
+
auggie: ['--acp'], cline: ['--acp'], devin: ['--permission-mode', 'dangerous', '--respect-workspace-trust', 'false', 'acp'],
|
|
16
|
+
goose: ['acp'], hermes: ['acp', '--accept-hooks'], kilo: ['acp'], kimi: ['acp'], kiro: ['acp'], reasonix: ['acp'], 'trae-cli': ['acp', 'serve'], vibe: [],
|
|
17
|
+
};
|
|
18
|
+
const CLI_ARGS = {
|
|
19
|
+
// UNVERIFIED adapters: commands documented by upstream/the survey; schemas need installed checks.
|
|
20
|
+
amp: ['plugins', 'show-agent-options', '--json'], crush: ['models'], deepseek: ['models'], qoder: ['--list-models'],
|
|
21
|
+
};
|
|
22
|
+
export function modelProbeTimeoutMs(agentId) {
|
|
23
|
+
if (ACP_ARGS[agentId] || ['claude', 'codebuddy', 'pi', 'copilot', 'droid'].includes(agentId))
|
|
24
|
+
return 15_000;
|
|
25
|
+
return 10_000;
|
|
26
|
+
}
|
|
27
|
+
/** Shared CLI-subcommand adapter. Output parsers are pure def-owned functions, not vendor tools. */
|
|
28
|
+
export async function probeCliSubcommand(context, config, deps, signal) {
|
|
29
|
+
// Reserve two seconds of the outer budget for CLI-owned cache recovery. The inner
|
|
30
|
+
// abort signal stops only the listing; the parent remains usable for reading fallback files.
|
|
31
|
+
const { stdout } = await withModelProbeTimeout(active => deps.process.run({ context, args: config.args || [], signal: active, timeoutMs: 8_000 }), 8_000, signal);
|
|
32
|
+
const models = config.parse?.({ stdout });
|
|
33
|
+
if (!models)
|
|
34
|
+
throw new ModelProbeError('malformed-response');
|
|
35
|
+
const defaultSelectionId = stdout.match(/^\s*Default model:\s*(\S+)\s*$/mi)?.[1];
|
|
36
|
+
return { models, source: 'cli', coverage: 'account', ...(defaultSelectionId ? { defaultSelectionId } : {}) };
|
|
37
|
+
}
|
|
38
|
+
/** ACP can create CLI session state/start services. It never sends a user prompt. */
|
|
39
|
+
export async function probeAcpMetadata(context, args, deps, signal) {
|
|
40
|
+
return deps.acp.probe({ context, args, signal });
|
|
41
|
+
}
|
|
42
|
+
/** Keep selection IDs, resolved concrete IDs and display names, including capability variants.
|
|
43
|
+
* Default-policy display names become the exact resolved ID; malformed responses return null.
|
|
44
|
+
* @complexity O(s + n) time and space in stdout length s and listed models n.
|
|
45
|
+
*/
|
|
46
|
+
export function parseClaudeInitializeMetadata(stdout) {
|
|
47
|
+
for (const line of stdout.split('\n')) {
|
|
48
|
+
let obj;
|
|
49
|
+
try {
|
|
50
|
+
obj = JSON.parse(line);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (obj?.type !== 'control_response')
|
|
56
|
+
continue;
|
|
57
|
+
const response = obj.response;
|
|
58
|
+
const rows = response?.subtype === 'success' ? response.response?.models : undefined;
|
|
59
|
+
if (!Array.isArray(rows))
|
|
60
|
+
continue;
|
|
61
|
+
const models = [];
|
|
62
|
+
let defaultSelectionId;
|
|
63
|
+
for (const row of rows) {
|
|
64
|
+
if (!row || typeof row.value !== 'string')
|
|
65
|
+
continue;
|
|
66
|
+
const id = row.value.trim();
|
|
67
|
+
const resolvedId = typeof row.resolvedModel === 'string' ? row.resolvedModel.trim() : undefined;
|
|
68
|
+
const label = typeof row.displayName === 'string' && row.displayName.trim() ? row.displayName.trim() : resolvedId || id;
|
|
69
|
+
if (id === 'default') {
|
|
70
|
+
// This displayName describes the CLI's default policy, not the model it resolves to.
|
|
71
|
+
if (resolvedId && modelIdentityKind(resolvedId) === 'concrete') {
|
|
72
|
+
defaultSelectionId = resolvedId;
|
|
73
|
+
models.push({ id: resolvedId, label: resolvedId, identityKind: 'concrete' });
|
|
74
|
+
}
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
models.push({ id, label, identityKind: id === resolvedId ? 'concrete' : resolvedId ? 'alias' : modelIdentityKind(id), ...(resolvedId ? { resolvedId } : {}) });
|
|
78
|
+
if (resolvedId && id !== resolvedId)
|
|
79
|
+
models.push({ id: resolvedId, label, identityKind: modelIdentityKind(resolvedId) });
|
|
80
|
+
}
|
|
81
|
+
return { models, source: 'rpc', coverage: 'account', ...(defaultSelectionId ? { defaultSelectionId } : {}) };
|
|
82
|
+
}
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
/** Shared initialize category; CodeBuddy compatibility remains UNVERIFIED locally. */
|
|
86
|
+
export async function probeClaudeInitialize(context, deps, signal) {
|
|
87
|
+
const args = [...CLAUDE_CODE_INITIALIZE_ARGS];
|
|
88
|
+
if (context.settingSources)
|
|
89
|
+
args.push('--setting-sources', context.settingSources.join(','));
|
|
90
|
+
// The last settings argument must still disable hooks; the probe never executes hooks.
|
|
91
|
+
const raw = context.settings ? (context.settings.trim().startsWith('{') ? context.settings : await deps.fs.read(context.settings, { signal })) : null;
|
|
92
|
+
const settings = raw ? parseSettingsJson(raw) : undefined;
|
|
93
|
+
if (settings)
|
|
94
|
+
args.push('--settings', JSON.stringify({ ...settings, disableAllHooks: true }));
|
|
95
|
+
const result = await deps.process.run({ context, args, stdin: CLAUDE_CODE_INITIALIZE_REQUEST, signal, timeoutMs: 15_000 });
|
|
96
|
+
const metadata = parseClaudeInitializeMetadata(result.stdout);
|
|
97
|
+
if (!metadata?.models.length)
|
|
98
|
+
throw new ModelProbeError('malformed-response');
|
|
99
|
+
return metadata;
|
|
100
|
+
}
|
|
101
|
+
export async function probeProviderApi(agentId, context, deps, signal) {
|
|
102
|
+
const config = await readModelConfiguration(agentId, context, deps, signal);
|
|
103
|
+
const effectiveContext = config.selection ? { ...context, model: config.selection } : context;
|
|
104
|
+
const input = await deps.credential.connection({ agentId, context: effectiveContext, signal });
|
|
105
|
+
if (!input)
|
|
106
|
+
throw new ModelProbeError('auth-missing');
|
|
107
|
+
if (input.protocol === 'bedrock' || input.protocol === 'azure')
|
|
108
|
+
throw new ModelProbeError('unsupported-version');
|
|
109
|
+
const result = await deps.http.list({ ...input, signal });
|
|
110
|
+
if (!result.ok || !result.models)
|
|
111
|
+
throw new ModelProbeError(result.kind === 'auth_failed' ? 'auth-missing' : result.kind === 'timeout' ? 'timeout' : 'offline');
|
|
112
|
+
const prefix = agentId === 'aider' && config.selection?.includes('/') ? config.selection.slice(0, config.selection.indexOf('/') + 1) : '';
|
|
113
|
+
return { models: result.models.map((row) => ({ ...row, id: prefix && !row.id.startsWith(prefix) ? `${prefix}${row.id}` : row.id })), source: 'provider-api', coverage: 'provider' };
|
|
114
|
+
}
|
|
115
|
+
/** Caches are partial observed catalogs, always stale, never silently promoted to live. */
|
|
116
|
+
export async function probeCliCache(agentId, context, deps, signal) {
|
|
117
|
+
const home = context.env.HOME || homedir();
|
|
118
|
+
const path = agentId === 'codex' ? join(context.env.CODEX_HOME || join(home, '.codex'), 'models_cache.json')
|
|
119
|
+
: agentId === 'opencode' ? join(context.env.XDG_CACHE_HOME || join(home, '.cache'), 'opencode/models.json')
|
|
120
|
+
: agentId === 'claude' ? join(context.env.CLAUDE_CONFIG_DIR || home, '.claude.json')
|
|
121
|
+
: agentId === 'codebuddy' ? join(home, '.codebuddy/settings.json') : '';
|
|
122
|
+
if (!path)
|
|
123
|
+
throw new ModelProbeError('unsupported-version');
|
|
124
|
+
const raw = await deps.fs.read(path, { signal });
|
|
125
|
+
const data = raw ? parseSettingsJson(raw) : null;
|
|
126
|
+
if (!data)
|
|
127
|
+
throw new ModelProbeError('offline');
|
|
128
|
+
if (agentId === 'codex' && raw)
|
|
129
|
+
return { models: configurationCacheModels(data.models), source: 'cli-cache', coverage: 'observed' };
|
|
130
|
+
if (agentId === 'opencode') {
|
|
131
|
+
const models = [];
|
|
132
|
+
for (const [provider, value] of Object.entries(data)) {
|
|
133
|
+
const entries = value && typeof value === 'object' ? value.models : undefined;
|
|
134
|
+
for (const [id, row] of Object.entries(entries || {}))
|
|
135
|
+
models.push({ id: `${provider}/${id}`, label: row.name || `${provider}/${id}`, provider, identityKind: modelIdentityKind(id) });
|
|
136
|
+
}
|
|
137
|
+
return { models, source: 'cli-cache', coverage: 'observed' };
|
|
138
|
+
}
|
|
139
|
+
// UNVERIFIED CodeBuddy cache schema. Configured model is weaker than an account catalog.
|
|
140
|
+
const entries = data.additionalModelOptionsCache;
|
|
141
|
+
return { models: configurationCacheModels(entries), source: 'cli-cache', coverage: 'observed' };
|
|
142
|
+
}
|
|
143
|
+
function configurationCacheModels(entries) {
|
|
144
|
+
if (!Array.isArray(entries))
|
|
145
|
+
return [];
|
|
146
|
+
return entries.flatMap((row) => {
|
|
147
|
+
const id = typeof row?.slug === 'string' ? row.slug : typeof row?.value === 'string' ? row.value : typeof row?.id === 'string' ? row.id : undefined;
|
|
148
|
+
return id ? [{ id, label: row.display_name || row.displayName || id, identityKind: modelIdentityKind(id) }] : [];
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
function parseGenericCliModels(stdout) {
|
|
152
|
+
let parsed;
|
|
153
|
+
try {
|
|
154
|
+
parsed = JSON.parse(stdout);
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
parsed = null;
|
|
158
|
+
}
|
|
159
|
+
if (parsed) {
|
|
160
|
+
const object = parsed;
|
|
161
|
+
const entries = Array.isArray(parsed) ? parsed : object.models;
|
|
162
|
+
if (Array.isArray(entries))
|
|
163
|
+
return entries.flatMap((row) => {
|
|
164
|
+
const id = typeof row === 'string' ? row : typeof row?.id === 'string' ? row.id : typeof row?.model === 'string' ? row.model : undefined;
|
|
165
|
+
return id ? [{ id, label: typeof row?.name === 'string' ? row.name : id, identityKind: modelIdentityKind(id) }] : [];
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
return stdout.split('\n').flatMap((line) => {
|
|
169
|
+
const match = line.trim().match(/^(?:[-*•]\s*)?([\w][\w./:[\]-]*)(?:\s+-\s+(.+))?$/);
|
|
170
|
+
if (!match || /^(models|available|provider|model)$/i.test(match[1]))
|
|
171
|
+
return [];
|
|
172
|
+
return [{ id: match[1], label: match[2] || match[1], identityKind: modelIdentityKind(match[1]) }];
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
export async function probeAgentCatalog(agentId, context, configuration, deps, signal) {
|
|
176
|
+
const acpArgs = ACP_ARGS[agentId];
|
|
177
|
+
// UNVERIFIED installed ACP agents: existing declared commands; metadata preserves native defaults.
|
|
178
|
+
if (acpArgs && agentId !== 'amr')
|
|
179
|
+
return probeAcpMetadata(context, acpArgs, deps, signal);
|
|
180
|
+
if (agentId === 'claude') {
|
|
181
|
+
try {
|
|
182
|
+
return await probeClaudeInitialize(context, deps, signal);
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
try {
|
|
186
|
+
return await probeProviderApi(agentId, context, deps, signal);
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
return probeCliCache(agentId, context, deps, signal);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
if (agentId === 'pi' || agentId === 'copilot' || agentId === 'droid')
|
|
194
|
+
return deps.rpc.probe({ agentId, context, signal });
|
|
195
|
+
// UNVERIFIED CLI-owned effective credential adapters; OAuth/unknown endpoints require host ports.
|
|
196
|
+
if (agentId === 'aider' || agentId === 'qwen' || agentId === 'mimo')
|
|
197
|
+
return probeProviderApi(agentId, context, deps, signal);
|
|
198
|
+
if (agentId === 'codebuddy') {
|
|
199
|
+
// UNVERIFIED: survey identifies initialize compatibility as a candidate, not a guarantee.
|
|
200
|
+
// Try the same prompt-free metadata category; failed compatibility stays explicitly offline.
|
|
201
|
+
try {
|
|
202
|
+
return await probeClaudeInitialize(context, deps, signal);
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
return probeCliCache(agentId, context, deps, signal);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
if (configuration.args) {
|
|
209
|
+
try {
|
|
210
|
+
return await probeCliSubcommand(context, configuration, deps, signal);
|
|
211
|
+
}
|
|
212
|
+
catch (error) {
|
|
213
|
+
if (agentId === 'codex' || agentId === 'opencode') {
|
|
214
|
+
try {
|
|
215
|
+
return await probeCliCache(agentId, context, deps, signal);
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
throw error;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
throw error;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (agentId === 'amr') {
|
|
225
|
+
// Every ACP def uses this category. Vela's documented list command remains a fallback
|
|
226
|
+
// when its ACP version omits the model catalog; launch still confirms explicit selection.
|
|
227
|
+
try {
|
|
228
|
+
const metadata = await probeAcpMetadata(context, ACP_ARGS.amr, deps, signal);
|
|
229
|
+
const first = metadata.models.find((row) => row.id !== 'default' && modelIdentityKind(row.id) === 'concrete');
|
|
230
|
+
if (!first)
|
|
231
|
+
throw new ModelProbeError('malformed-response');
|
|
232
|
+
return { ...metadata, defaultSelectionId: context.env.VELA_DEFAULT_MODEL || metadata.defaultSelectionId || first.id };
|
|
233
|
+
}
|
|
234
|
+
catch (error) {
|
|
235
|
+
if (signal.aborted)
|
|
236
|
+
throw error;
|
|
237
|
+
}
|
|
238
|
+
const result = await deps.process.run({ context, args: ['model', 'list', '--format', 'json'], signal, timeoutMs: 10_000 });
|
|
239
|
+
const models = configuration.parse?.({ stdout: result.stdout }) || [];
|
|
240
|
+
// Vela requires an explicit concrete set_model before prompting; retain the first native row.
|
|
241
|
+
const first = models.find((row) => row.id !== 'default');
|
|
242
|
+
return { models, source: 'cli', coverage: 'account', ...(first ? { defaultSelectionId: context.env.VELA_DEFAULT_MODEL || first.id } : {}) };
|
|
243
|
+
}
|
|
244
|
+
const args = CLI_ARGS[agentId];
|
|
245
|
+
if (!args)
|
|
246
|
+
throw new ModelProbeError('unsupported-version');
|
|
247
|
+
return probeCliSubcommand(context, { ...configuration, args, parse: ({ stdout }) => parseGenericCliModels(stdout) }, deps, signal);
|
|
248
|
+
}
|
|
249
|
+
function runProcess(input) {
|
|
250
|
+
if (input.stdin === undefined)
|
|
251
|
+
return execAgentFile({ command: input.context.executable, args: [...input.args] }, { options: {
|
|
252
|
+
cwd: input.context.cwd, env: input.context.env, signal: input.signal,
|
|
253
|
+
timeout: input.timeoutMs, killSignal: 'SIGKILL', maxBuffer: 8 * 1024 * 1024,
|
|
254
|
+
} }).then(({ stdout, stderr }) => ({ stdout: String(stdout), stderr: String(stderr) }));
|
|
255
|
+
return new Promise((resolve, reject) => {
|
|
256
|
+
let completed = false;
|
|
257
|
+
let captured = '';
|
|
258
|
+
const invocation = createCommandInvocation({ command: input.context.executable, args: [...input.args], env: input.context.env });
|
|
259
|
+
const tree = input.terminateProcessTree && process.platform !== 'win32';
|
|
260
|
+
let treeTimer;
|
|
261
|
+
const killTree = () => { if (tree && child.pid) {
|
|
262
|
+
try {
|
|
263
|
+
process.kill(-child.pid, 'SIGKILL');
|
|
264
|
+
}
|
|
265
|
+
catch { }
|
|
266
|
+
} };
|
|
267
|
+
// execFile forwards `detached` to spawn at runtime, but its typings omit it; a typed
|
|
268
|
+
// variable (not a literal) keeps the process-group option without a cast.
|
|
269
|
+
const options = { cwd: input.context.cwd, env: input.context.env,
|
|
270
|
+
timeout: input.timeoutMs, killSignal: 'SIGKILL', signal: input.signal, maxBuffer: 8 * 1024 * 1024,
|
|
271
|
+
detached: Boolean(tree),
|
|
272
|
+
windowsVerbatimArguments: invocation.windowsVerbatimArguments, };
|
|
273
|
+
const child = execFile(invocation.command, invocation.args, options, (error, stdout, stderr) => {
|
|
274
|
+
if (treeTimer)
|
|
275
|
+
clearTimeout(treeTimer);
|
|
276
|
+
input.signal.removeEventListener('abort', killTree);
|
|
277
|
+
if (error)
|
|
278
|
+
killTree();
|
|
279
|
+
error && !completed ? reject(error) : resolve({ stdout: String(stdout), stderr: String(stderr) });
|
|
280
|
+
});
|
|
281
|
+
if (tree) {
|
|
282
|
+
input.signal.addEventListener('abort', killTree, { once: true });
|
|
283
|
+
treeTimer = setTimeout(killTree, input.timeoutMs);
|
|
284
|
+
if (input.signal.aborted)
|
|
285
|
+
killTree();
|
|
286
|
+
}
|
|
287
|
+
if (input.complete)
|
|
288
|
+
child.stdout?.on('data', (chunk) => {
|
|
289
|
+
captured += String(chunk);
|
|
290
|
+
if (input.complete?.(captured)) {
|
|
291
|
+
completed = true;
|
|
292
|
+
child.kill('SIGKILL');
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
child.stdin?.on('error', () => { });
|
|
296
|
+
child.stdin?.end(input.stdin);
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
export const defaultModelDiscoveryDeps = {
|
|
300
|
+
process: { run: runProcess },
|
|
301
|
+
fs: {
|
|
302
|
+
read: (path, { signal }) => readFile(path, { encoding: 'utf8', signal }).catch(() => null),
|
|
303
|
+
async projectRoot({ cwd, signal }) {
|
|
304
|
+
let directory = cwd;
|
|
305
|
+
for (let depth = 0; depth < 64 && !signal.aborted; depth++) {
|
|
306
|
+
if (await stat(join(directory, '.git')).then(() => true, () => false))
|
|
307
|
+
return directory;
|
|
308
|
+
const parent = dirname(directory);
|
|
309
|
+
if (parent === directory)
|
|
310
|
+
break;
|
|
311
|
+
directory = parent;
|
|
312
|
+
}
|
|
313
|
+
return null;
|
|
314
|
+
},
|
|
315
|
+
},
|
|
316
|
+
http: { list: (input) => { const { protocol, baseUrl, apiKey, ...optional } = input; return listProviderModels({ protocol, baseUrl, apiKey }, optional); } },
|
|
317
|
+
credential: { async connection({ agentId, context }) {
|
|
318
|
+
// Deliberately do not steal tokens from another CLI's store. Hosts can inject keychain/OAuth
|
|
319
|
+
// handles. BYOK env is usable only when it identifies this CLI's effective provider.
|
|
320
|
+
const env = context.env;
|
|
321
|
+
if (agentId === 'claude' && env.ANTHROPIC_API_KEY)
|
|
322
|
+
return { protocol: 'anthropic', baseUrl: env.ANTHROPIC_BASE_URL || 'https://api.anthropic.com', apiKey: env.ANTHROPIC_API_KEY };
|
|
323
|
+
const selected = context.model || (agentId === 'aider' ? env.AIDER_MODEL : agentId === 'qwen' ? env.QWEN_MODEL || env.OPENAI_MODEL : undefined);
|
|
324
|
+
if (!selected)
|
|
325
|
+
return null;
|
|
326
|
+
if (/^(anthropic\/|claude-)/.test(selected) && env.ANTHROPIC_API_KEY)
|
|
327
|
+
return { protocol: 'anthropic', baseUrl: env.ANTHROPIC_BASE_URL || 'https://api.anthropic.com', apiKey: env.ANTHROPIC_API_KEY };
|
|
328
|
+
if (/^(gemini\/|google\/|gemini-)/.test(selected) && env.GEMINI_API_KEY)
|
|
329
|
+
return { protocol: 'google', baseUrl: 'https://generativelanguage.googleapis.com', apiKey: env.GEMINI_API_KEY };
|
|
330
|
+
if (/^openrouter\//.test(selected) && env.OPENROUTER_API_KEY)
|
|
331
|
+
return { protocol: 'openai', baseUrl: env.OPENROUTER_API_BASE || 'https://openrouter.ai/api/v1', apiKey: env.OPENROUTER_API_KEY };
|
|
332
|
+
if ((/^(openai\/|gpt-|o\d)/.test(selected) || agentId === 'qwen' && (env.OPENAI_MODEL || env.OPENAI_BASE_URL)) && env.OPENAI_API_KEY)
|
|
333
|
+
return { protocol: 'openai', baseUrl: env.OPENAI_BASE_URL || env.OPENAI_API_BASE || 'https://api.openai.com/v1', apiKey: env.OPENAI_API_KEY };
|
|
334
|
+
return null;
|
|
335
|
+
} },
|
|
336
|
+
acp: { async probe({ context, args, signal }) {
|
|
337
|
+
const invocation = createCommandInvocation({ command: context.executable, args: [...args], env: context.env });
|
|
338
|
+
const result = await detectAcpModelMetadata({ bin: invocation.command, args: invocation.args }, { cwd: context.cwd, env: context.env, timeoutMs: 15_000, signal });
|
|
339
|
+
return { models: result.models, source: 'rpc', coverage: 'account', ...(result.currentModelId ? { defaultSelectionId: result.currentModelId } : {}) };
|
|
340
|
+
} },
|
|
341
|
+
rpc: { async probe({ agentId, context, signal }) {
|
|
342
|
+
// UNVERIFIED locally: use official prompt-free SDK interfaces, not invented CLI flags.
|
|
343
|
+
if (agentId === 'copilot')
|
|
344
|
+
return probeCopilotSdk(context, signal);
|
|
345
|
+
if (agentId === 'droid')
|
|
346
|
+
return probeDroidSdk(context, defaultModelDiscoveryDeps, signal);
|
|
347
|
+
if (agentId !== 'pi')
|
|
348
|
+
throw new ModelProbeError('unsupported-version');
|
|
349
|
+
const { stdout } = await defaultModelDiscoveryDeps.process.run({ context, args: ['--mode', 'rpc'], stdin: '{"type":"get_available_models","id":"catalog"}\n{"type":"get_state","id":"state"}\n', signal, timeoutMs: 15_000,
|
|
350
|
+
complete: (output) => {
|
|
351
|
+
try {
|
|
352
|
+
const metadata = parsePiRpcMetadata(output);
|
|
353
|
+
return Boolean(metadata.defaultSelectionId);
|
|
354
|
+
}
|
|
355
|
+
catch {
|
|
356
|
+
return false;
|
|
357
|
+
}
|
|
358
|
+
} });
|
|
359
|
+
return parsePiRpcMetadata(stdout);
|
|
360
|
+
} },
|
|
361
|
+
clock: { now: Date.now }, cache: new Map(),
|
|
362
|
+
};
|
|
363
|
+
/** Pi's native RPC supplies catalog AND current provider/id without making an inference call. */
|
|
364
|
+
export function parsePiRpcMetadata(stdout) {
|
|
365
|
+
const models = [];
|
|
366
|
+
let defaultSelectionId;
|
|
367
|
+
for (const line of stdout.split('\n')) {
|
|
368
|
+
let response;
|
|
369
|
+
try {
|
|
370
|
+
response = JSON.parse(line);
|
|
371
|
+
}
|
|
372
|
+
catch {
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
if (response.type !== 'response' || !response.success)
|
|
376
|
+
continue;
|
|
377
|
+
if (response.command === 'get_available_models' && Array.isArray(response.data?.models)) {
|
|
378
|
+
for (const raw of response.data.models) {
|
|
379
|
+
const row = raw;
|
|
380
|
+
if (row.provider && row.id)
|
|
381
|
+
models.push({ id: `${row.provider}/${row.id}`, label: row.name || row.id, provider: row.provider, identityKind: modelIdentityKind(row.id) });
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
if (response.command === 'get_state' && response.data?.model?.provider && response.data.model.id)
|
|
385
|
+
defaultSelectionId = `${response.data.model.provider}/${response.data.model.id}`;
|
|
386
|
+
}
|
|
387
|
+
if (!models.length)
|
|
388
|
+
throw new ModelProbeError('malformed-response');
|
|
389
|
+
return { models, source: 'rpc', coverage: 'account', ...(defaultSelectionId ? { defaultSelectionId } : {}) };
|
|
390
|
+
}
|
|
391
|
+
//# sourceMappingURL=model-discovery-adapters.js.map
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ModelDiscoveryContext, ModelDiscoveryDeps } from './model-discovery-types.js';
|
|
2
|
+
type ConfigFile = {
|
|
3
|
+
path: string;
|
|
4
|
+
format: 'json' | 'toml' | 'yaml';
|
|
5
|
+
key?: string;
|
|
6
|
+
};
|
|
7
|
+
/** Config-file resolvers read only settings, never auth-store contents. Precedence is applied
|
|
8
|
+
* from global to workspace/local. Unknown native priorities remain unresolved, not invented. */
|
|
9
|
+
export declare function modelConfigFiles(agentId: string, context: ModelDiscoveryContext): ConfigFile[];
|
|
10
|
+
export declare function parseSettingsJson(raw: string): Record<string, unknown> | null;
|
|
11
|
+
export declare function parseConfiguredModel(file: ConfigFile, raw: string, profile?: string): string | undefined;
|
|
12
|
+
export declare function readModelConfiguration(agentId: string, context: ModelDiscoveryContext, deps: ModelDiscoveryDeps, signal: AbortSignal): Promise<{
|
|
13
|
+
selection?: string;
|
|
14
|
+
fingerprintMaterial: string;
|
|
15
|
+
}>;
|
|
16
|
+
export {};
|
|
17
|
+
//# sourceMappingURL=model-discovery-config.d.ts.map
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
const json = (path, key = 'model') => ({ path, format: 'json', key });
|
|
4
|
+
/** Config-file resolvers read only settings, never auth-store contents. Precedence is applied
|
|
5
|
+
* from global to workspace/local. Unknown native priorities remain unresolved, not invented. */
|
|
6
|
+
export function modelConfigFiles(agentId, context) {
|
|
7
|
+
const env = context.env;
|
|
8
|
+
const home = env.HOME || homedir();
|
|
9
|
+
const user = (name) => join(home, name);
|
|
10
|
+
const local = (name) => join(context.cwd, name);
|
|
11
|
+
const root = (name) => join(context.projectRoot || context.cwd, name);
|
|
12
|
+
const configHome = env.XDG_CONFIG_HOME || user('.config');
|
|
13
|
+
switch (agentId) {
|
|
14
|
+
case 'codex': {
|
|
15
|
+
const root = env.CODEX_HOME || user('.codex');
|
|
16
|
+
return [{ path: join(root, 'config.toml'), format: 'toml' },
|
|
17
|
+
...(context.profile ? [{ path: join(root, `${context.profile}.config.toml`), format: 'toml' }] : []),
|
|
18
|
+
{ path: local('.codex/config.toml'), format: 'toml' }];
|
|
19
|
+
}
|
|
20
|
+
case 'claude': {
|
|
21
|
+
const claudeRoot = env.CLAUDE_CONFIG_DIR || user('.claude');
|
|
22
|
+
const sources = context.settingSources ?? ['user', 'project', 'local'];
|
|
23
|
+
return [...(sources.includes('user') ? [json(join(claudeRoot, 'settings.json'))] : []),
|
|
24
|
+
...(sources.includes('project') ? [json(root('.claude/settings.json'))] : []),
|
|
25
|
+
...(sources.includes('local') ? [json(root('.claude/settings.local.json'))] : [])];
|
|
26
|
+
}
|
|
27
|
+
case 'antigravity': return [json(user('.gemini/antigravity-cli/settings.json'))];
|
|
28
|
+
case 'opencode': return [json(join(env.XDG_STATE_HOME || user('.local/state'), 'opencode/model.json'), 'recent'), json(join(configHome, 'opencode/opencode.json')), json(join(configHome, 'opencode/opencode.jsonc')), json(root('opencode.json')), json(root('opencode.jsonc')), json(local('opencode.json')), json(local('opencode.jsonc'))];
|
|
29
|
+
case 'pi': return [json(join(env.PI_CODING_AGENT_DIR || user('.pi/agent'), 'settings.json'), 'defaultModel'), json(local('.pi/settings.json'), 'defaultModel')];
|
|
30
|
+
case 'aider': return context.configPath ? [{ path: context.configPath, format: 'yaml' }] : [{ path: user('.aider.conf.yml'), format: 'yaml' }, { path: root('.aider.conf.yml'), format: 'yaml' }, { path: local('.aider.conf.yml'), format: 'yaml' }];
|
|
31
|
+
// UNVERIFIED locally: the survey documents these settings, not installed schema behavior.
|
|
32
|
+
case 'grok-build': return [{ path: user('.grok/config.toml'), format: 'toml', key: 'models.model' }];
|
|
33
|
+
case 'codebuddy': return [json(user('.codebuddy/settings.json')), json(local('.codebuddy/settings.json')), json(local('.codebuddy/settings.local.json'))];
|
|
34
|
+
case 'copilot': return [json(user('.copilot/config.json'))];
|
|
35
|
+
case 'cursor-agent': return [json(user('.cursor/cli-config.json'))];
|
|
36
|
+
case 'droid': return [json(user('.factory/settings.json'), 'sessionDefaultSettings.model')];
|
|
37
|
+
case 'qoder': return [json(join(env.QODER_CONFIG_DIR || user('.qoder'), 'settings.json'), 'model.name'), json(local('.qoder/settings.json'), 'model.name'), json(local('.qoder/settings.local.json'), 'model.name')];
|
|
38
|
+
case 'qwen': return [json(user('.qwen/settings.json'), 'model.name'), json(local('.qwen/settings.json'), 'model.name')];
|
|
39
|
+
case 'deepseek': return [{ path: user('.deepseek/config.toml'), format: 'toml' }, { path: user('.codewhale/config.toml'), format: 'toml' }];
|
|
40
|
+
case 'mimo': return [json(join(configHome, 'mimocode/mimocode.json')), json(local('.mimocode/mimocode.jsonc'))];
|
|
41
|
+
case 'crush': return [json(join(configHome, 'crush/crush.json'), 'models.large.model'), json(local('crush.json'), 'models.large.model')];
|
|
42
|
+
default: return [];
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export function parseSettingsJson(raw) {
|
|
46
|
+
try {
|
|
47
|
+
// Preserve quoted strings (including URLs) while removing JSONC comments.
|
|
48
|
+
const stripped = raw.replace(/"(?:\\.|[^"\\])*"|\/\/[^\n]*|\/\*[\s\S]*?\*\//g, (token) => token.startsWith('"') ? token : '').replace(/,\s*([}\]])/g, '$1');
|
|
49
|
+
const value = JSON.parse(stripped);
|
|
50
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function scalar(value) { return typeof value === 'string' && value.trim() ? value.trim() : undefined; }
|
|
57
|
+
function dotted(value, key) {
|
|
58
|
+
return key.split('.').reduce((item, part) => item && typeof item === 'object' ? item[part] : undefined, value);
|
|
59
|
+
}
|
|
60
|
+
export function parseConfiguredModel(file, raw, profile) {
|
|
61
|
+
if (file.format === 'json') {
|
|
62
|
+
const settings = parseSettingsJson(raw);
|
|
63
|
+
const model = dotted(settings, file.key || 'model');
|
|
64
|
+
if (file.key === 'recent' && Array.isArray(model)) {
|
|
65
|
+
const first = model[0];
|
|
66
|
+
const provider = scalar(first?.providerID);
|
|
67
|
+
const id = scalar(first?.modelID);
|
|
68
|
+
return provider && id ? `${provider}/${id}` : undefined;
|
|
69
|
+
}
|
|
70
|
+
// Cursor's model object subkeys are undocumented: never guess which one is the effective id.
|
|
71
|
+
const id = scalar(model);
|
|
72
|
+
if (file.key === 'defaultModel' && id) {
|
|
73
|
+
const provider = scalar(settings?.defaultProvider);
|
|
74
|
+
return provider ? `${provider}/${id}` : undefined;
|
|
75
|
+
}
|
|
76
|
+
return id;
|
|
77
|
+
}
|
|
78
|
+
if (file.format === 'yaml')
|
|
79
|
+
return scalar(raw.match(/^model\s*:\s*["']?([^\n"'#]+)["']?\s*(?:#.*)?$/m)?.[1]);
|
|
80
|
+
let section = '';
|
|
81
|
+
let chosen;
|
|
82
|
+
let profileChoice;
|
|
83
|
+
for (const line of raw.split('\n')) {
|
|
84
|
+
const header = line.match(/^\s*\[([^\]]+)\]/);
|
|
85
|
+
if (header) {
|
|
86
|
+
section = header[1];
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
const match = line.match(/^\s*model\s*=\s*["']([^"']+)["']/);
|
|
90
|
+
if (!match)
|
|
91
|
+
continue;
|
|
92
|
+
if (profile && section === `profiles.${profile}`)
|
|
93
|
+
profileChoice = match[1];
|
|
94
|
+
if (section === (file.key === 'models.model' ? 'models' : ''))
|
|
95
|
+
chosen = match[1];
|
|
96
|
+
}
|
|
97
|
+
return profileChoice || chosen;
|
|
98
|
+
}
|
|
99
|
+
const MODEL_ENV = {
|
|
100
|
+
claude: ['ANTHROPIC_MODEL'], aider: ['AIDER_MODEL'], amr: ['VELA_DEFAULT_MODEL'],
|
|
101
|
+
deepseek: ['CODEWHALE_MODEL', 'DEEPSEEK_MODEL'], qwen: ['QWEN_MODEL', 'OPENAI_MODEL', 'ANTHROPIC_MODEL', 'GEMINI_MODEL'],
|
|
102
|
+
};
|
|
103
|
+
export async function readModelConfiguration(agentId, context, deps, signal) {
|
|
104
|
+
if (!context.projectRoot && ['claude', 'aider', 'opencode'].includes(agentId) && deps.fs.projectRoot) {
|
|
105
|
+
const projectRoot = await deps.fs.projectRoot({ cwd: context.cwd, signal });
|
|
106
|
+
if (projectRoot)
|
|
107
|
+
context = { ...context, projectRoot };
|
|
108
|
+
}
|
|
109
|
+
const files = modelConfigFiles(agentId, context);
|
|
110
|
+
const contents = await Promise.all(files.map(async (file) => ({ file, raw: await deps.fs.read(file.path, { signal }) })));
|
|
111
|
+
let extraMaterial = '';
|
|
112
|
+
let selection;
|
|
113
|
+
for (const { file, raw } of contents)
|
|
114
|
+
if (raw !== null)
|
|
115
|
+
selection = parseConfiguredModel(file, raw, context.profile) || selection;
|
|
116
|
+
if (agentId === 'pi') {
|
|
117
|
+
const effective = Object.assign({}, ...contents.map(({ raw }) => raw ? parseSettingsJson(raw) || {} : {}));
|
|
118
|
+
const provider = scalar(effective.defaultProvider);
|
|
119
|
+
const id = scalar(effective.defaultModel);
|
|
120
|
+
selection = provider && id ? `${provider}/${id}` : undefined;
|
|
121
|
+
}
|
|
122
|
+
const inline = agentId === 'opencode' ? context.env.OPENCODE_CONFIG_CONTENT : agentId === 'mimo' ? context.env.MIMOCODE_CONFIG_CONTENT : undefined;
|
|
123
|
+
if (inline)
|
|
124
|
+
selection = scalar(parseSettingsJson(inline)?.model) || selection;
|
|
125
|
+
for (const key of [...(MODEL_ENV[agentId] || [])].reverse())
|
|
126
|
+
selection = scalar(context.env[key]) || selection;
|
|
127
|
+
if (agentId === 'claude' && context.settings) {
|
|
128
|
+
const raw = context.settings.trim().startsWith('{') ? context.settings : await deps.fs.read(context.settings, { signal });
|
|
129
|
+
extraMaterial = raw || '';
|
|
130
|
+
if (raw)
|
|
131
|
+
selection = scalar(parseSettingsJson(raw)?.model) || selection;
|
|
132
|
+
}
|
|
133
|
+
selection = context.model || selection;
|
|
134
|
+
// Material is hashed by the orchestration layer and is never exposed over HTTP.
|
|
135
|
+
return { ...(selection ? { selection } : {}), fingerprintMaterial: JSON.stringify([contents.map(({ file, raw }) => [file.path, raw]), extraMaterial]) };
|
|
136
|
+
}
|
|
137
|
+
//# sourceMappingURL=model-discovery-config.js.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ModelDiscoveryContext, ModelDiscoveryDeps, ModelProbeResult } from './model-discovery-types.js';
|
|
2
|
+
/** UNVERIFIED locally. The host may install the official SDK or inject the RPC port.
|
|
3
|
+
* Use the CLI's authenticated server, not an unrelated HTTP token or an inference prompt.
|
|
4
|
+
* https://github.com/github/copilot-sdk/blob/main/nodejs/src/client.ts */
|
|
5
|
+
interface CopilotMetadataClient {
|
|
6
|
+
start(): Promise<unknown>;
|
|
7
|
+
listModels(): Promise<{
|
|
8
|
+
id: string;
|
|
9
|
+
name?: string;
|
|
10
|
+
}[]>;
|
|
11
|
+
forceStop(): Promise<unknown>;
|
|
12
|
+
}
|
|
13
|
+
export declare function probeCopilotSdk(context: ModelDiscoveryContext, signal: AbortSignal, optional?: {
|
|
14
|
+
createClient?: (context: ModelDiscoveryContext) => Promise<CopilotMetadataClient>;
|
|
15
|
+
}): Promise<ModelProbeResult>;
|
|
16
|
+
export declare function probeDroidSdk(context: ModelDiscoveryContext, deps: ModelDiscoveryDeps, signal: AbortSignal): Promise<ModelProbeResult>;
|
|
17
|
+
export {};
|
|
18
|
+
//# sourceMappingURL=model-discovery-sdk.d.ts.map
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { ModelProbeError, modelIdentityKind } from './model-discovery.js';
|
|
2
|
+
export async function probeCopilotSdk(context, signal, optional = {}) {
|
|
3
|
+
const client = await (optional.createClient ?? createCopilotMetadataClient)(context);
|
|
4
|
+
if (signal.aborted) {
|
|
5
|
+
await client.forceStop().catch(() => { });
|
|
6
|
+
throw new ModelProbeError('timeout');
|
|
7
|
+
}
|
|
8
|
+
const stop = () => { void Promise.resolve(client.forceStop()).catch(() => { }); };
|
|
9
|
+
signal.addEventListener('abort', stop, { once: true });
|
|
10
|
+
try {
|
|
11
|
+
await client.start();
|
|
12
|
+
if (signal.aborted)
|
|
13
|
+
throw new ModelProbeError('timeout');
|
|
14
|
+
const rows = await client.listModels();
|
|
15
|
+
return { models: rows.map((row) => ({ id: row.id, label: row.name || row.id, identityKind: modelIdentityKind(row.id) })), source: 'rpc', coverage: 'account' };
|
|
16
|
+
}
|
|
17
|
+
finally {
|
|
18
|
+
signal.removeEventListener('abort', stop);
|
|
19
|
+
// forceStop also closes the child when a failed handshake never reached connected state.
|
|
20
|
+
await client.forceStop().catch(() => { });
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
async function createCopilotMetadataClient(context) {
|
|
24
|
+
const packageName = '@github/copilot-sdk';
|
|
25
|
+
const sdk = await import(packageName);
|
|
26
|
+
const env = Object.fromEntries(Object.entries(context.env).filter((entry) => typeof entry[1] === 'string'));
|
|
27
|
+
// RuntimeConnection is the current SDK API; older SDKs expose cliPath/cwd/useStdio.
|
|
28
|
+
const connection = sdk.RuntimeConnection?.forStdio({ path: context.executable, env });
|
|
29
|
+
return new sdk.CopilotClient(connection
|
|
30
|
+
? { connection, workingDirectory: context.cwd, autoRestart: false }
|
|
31
|
+
: { cliPath: context.executable, cwd: context.cwd, env, useStdio: true, autoRestart: false });
|
|
32
|
+
}
|
|
33
|
+
/** UNVERIFIED locally. Official Python SDK metadata bridge; no session/inference call.
|
|
34
|
+
* SDK installation belongs to the host's Python environment.
|
|
35
|
+
* https://docs.factory.com/sdk/python#discover-available-models */
|
|
36
|
+
const DROID_METADATA_SCRIPT = `import asyncio, json, os, sys
|
|
37
|
+
from droid_sdk import Runtime, list_models
|
|
38
|
+
async def main():
|
|
39
|
+
rows = await list_models(cwd=os.getcwd(), runtime=Runtime(executable=sys.argv[1]))
|
|
40
|
+
print(json.dumps({"models": [{"id": m.id, "name": m.display_name, "provider": str(m.provider)} for m in rows if not m.disabled]}))
|
|
41
|
+
asyncio.run(main())
|
|
42
|
+
`;
|
|
43
|
+
export async function probeDroidSdk(context, deps, signal) {
|
|
44
|
+
const { stdout } = await deps.process.run({ context: { ...context, executable: 'python3' }, args: ['-c', DROID_METADATA_SCRIPT, context.executable], stdin: '', signal, timeoutMs: 15_000, terminateProcessTree: true });
|
|
45
|
+
let data;
|
|
46
|
+
try {
|
|
47
|
+
data = JSON.parse(stdout);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
throw new ModelProbeError('malformed-response');
|
|
51
|
+
}
|
|
52
|
+
if (!Array.isArray(data.models))
|
|
53
|
+
throw new ModelProbeError('malformed-response');
|
|
54
|
+
return { models: data.models.filter((row) => typeof row.id === 'string').map((row) => ({ id: row.id, label: row.name || row.id, provider: row.provider, identityKind: modelIdentityKind(row.id) })), source: 'rpc', coverage: 'account' };
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=model-discovery-sdk.js.map
|