@jini-ai/agent-runtime 0.4.4 → 0.4.6
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 +5 -0
- package/dist/agent-protocol/acp/models.d.ts +11 -0
- package/dist/agent-protocol/acp/models.js +12 -3
- package/dist/capabilities.d.ts +1 -8
- package/dist/capabilities.js +8 -1
- package/dist/claude-code-models.d.ts +13 -1
- package/dist/claude-code-models.js +15 -17
- package/dist/claude-segment-usage.d.ts +9 -2
- package/dist/claude-segment-usage.js +20 -4
- package/dist/claude-stream.d.ts +4 -0
- package/dist/claude-stream.js +5 -1
- package/dist/defs/aider.js +1 -1
- package/dist/defs/amp.js +1 -1
- package/dist/defs/amr.js +1 -1
- package/dist/defs/antigravity.js +1 -1
- package/dist/defs/auggie.js +1 -1
- package/dist/defs/claude.d.ts +2 -0
- package/dist/defs/claude.js +11 -3
- package/dist/defs/cline.js +1 -1
- package/dist/defs/codebuddy.js +2 -2
- package/dist/defs/codex.js +1 -1
- package/dist/defs/copilot.js +1 -1
- package/dist/defs/crush.js +1 -1
- package/dist/defs/cursor-agent.js +2 -2
- package/dist/defs/deepseek.js +1 -1
- package/dist/defs/devin.js +1 -1
- package/dist/defs/droid.js +1 -1
- package/dist/defs/goose.js +1 -1
- package/dist/defs/grok-build.js +1 -1
- package/dist/defs/hermes.js +1 -1
- package/dist/defs/kilo.js +1 -1
- package/dist/defs/kimi.js +1 -1
- package/dist/defs/kiro.js +1 -1
- package/dist/defs/mimo.js +1 -1
- package/dist/defs/opencode.js +2 -2
- package/dist/defs/pi.js +1 -1
- package/dist/defs/qoder.js +1 -1
- package/dist/defs/qwen.js +1 -1
- package/dist/defs/reasonix.js +1 -1
- package/dist/defs/trae-cli.js +1 -1
- package/dist/defs/vibe.js +1 -1
- package/dist/detection.d.ts +28 -4
- package/dist/detection.js +82 -43
- package/dist/diagnostics.d.ts +6 -1
- package/dist/diagnostics.js +2 -2
- package/dist/env.js +1 -1
- package/dist/executables.d.ts +15 -9
- package/dist/executables.js +73 -65
- package/dist/index.d.ts +3 -2
- package/dist/index.js +3 -2
- package/dist/invocation.d.ts +24 -2
- package/dist/invocation.js +19 -7
- package/dist/launch.d.ts +2 -1
- package/dist/launch.js +2 -2
- package/dist/model-discovery-adapters.d.ts +6 -2
- package/dist/model-discovery-adapters.js +64 -51
- package/dist/model-discovery-native.d.ts +14 -0
- package/dist/model-discovery-native.js +22 -0
- package/dist/model-discovery-sdk.js +1 -1
- package/dist/model-discovery-shared.d.ts +14 -0
- package/dist/model-discovery-shared.js +53 -0
- package/dist/model-discovery-types.d.ts +26 -0
- package/dist/model-discovery.d.ts +17 -22
- package/dist/model-discovery.js +18 -69
- package/dist/models.d.ts +9 -4
- package/dist/models.js +11 -11
- package/dist/probe-scheduler.d.ts +19 -0
- package/dist/probe-scheduler.js +30 -0
- package/dist/runtime-state.d.ts +41 -0
- package/dist/runtime-state.js +31 -0
- package/dist/types.d.ts +4 -0
- package/package.json +36 -9
|
@@ -1,15 +1,18 @@
|
|
|
1
|
+
import { defaultRuntimeState } from './runtime-state.js';
|
|
2
|
+
import { VENDOR_API_BASE_URLS } from '@jini-ai/integrations/media-providers/catalog';
|
|
1
3
|
import { execFile } from 'node:child_process';
|
|
2
4
|
import { readFile, stat } from 'node:fs/promises';
|
|
3
5
|
import { homedir } from 'node:os';
|
|
4
6
|
import { join, dirname } from 'node:path';
|
|
5
|
-
import { createCommandInvocation } from '@jini-ai/platform';
|
|
7
|
+
import { createCommandInvocation } from '@jini-ai/platform/command';
|
|
6
8
|
import { detectAcpModelMetadata } from './agent-protocol/acp/models.js';
|
|
7
9
|
import { CLAUDE_CODE_INITIALIZE_ARGS, CLAUDE_CODE_INITIALIZE_REQUEST } from './claude-code-models.js';
|
|
8
10
|
import { listProviderModels } from './providers/model-catalog.js';
|
|
9
11
|
import { execAgentFile } from './invocation.js';
|
|
12
|
+
import { scheduleProbeProcess } from './probe-scheduler.js';
|
|
10
13
|
import { probeCopilotSdk, probeDroidSdk } from './model-discovery-sdk.js';
|
|
11
14
|
import { parseSettingsJson, readModelConfiguration } from './model-discovery-config.js';
|
|
12
|
-
import { ModelProbeError, modelIdentityKind, withModelProbeTimeout } from './model-discovery.js';
|
|
15
|
+
import { ModelProbeError, modelIdentityKind, withModelProbeTimeout } from './model-discovery-shared.js';
|
|
13
16
|
const ACP_ARGS = {
|
|
14
17
|
amr: ['agent', 'run', '--runtime', 'opencode'],
|
|
15
18
|
auggie: ['--acp'], cline: ['--acp'], devin: ['--permission-mode', 'dangerous', '--respect-workspace-trust', 'false', 'acp'],
|
|
@@ -252,49 +255,53 @@ function runProcess(input) {
|
|
|
252
255
|
cwd: input.context.cwd, env: input.context.env, signal: input.signal,
|
|
253
256
|
timeout: input.timeoutMs, killSignal: 'SIGKILL', maxBuffer: 8 * 1024 * 1024,
|
|
254
257
|
} }).then(({ stdout, stderr }) => ({ stdout: String(stdout), stderr: String(stderr) }));
|
|
255
|
-
return new Promise((resolve, reject) => {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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');
|
|
258
|
+
return scheduleProbeProcess({ work: () => new Promise((resolve, reject) => {
|
|
259
|
+
let completed = false;
|
|
260
|
+
let captured = '';
|
|
261
|
+
const invocation = createCommandInvocation({ command: input.context.executable, args: [...input.args], env: input.context.env });
|
|
262
|
+
const tree = input.terminateProcessTree && process.platform !== 'win32';
|
|
263
|
+
let treeTimer;
|
|
264
|
+
const killTree = () => { if (tree && child.pid) {
|
|
265
|
+
try {
|
|
266
|
+
process.kill(-child.pid, 'SIGKILL');
|
|
293
267
|
}
|
|
268
|
+
catch { }
|
|
269
|
+
} };
|
|
270
|
+
// execFile forwards `detached` to spawn at runtime, but its typings omit it; a typed
|
|
271
|
+
// variable (not a literal) keeps the process-group option without a cast.
|
|
272
|
+
const options = { cwd: input.context.cwd, env: input.context.env,
|
|
273
|
+
timeout: input.timeoutMs, killSignal: 'SIGKILL', signal: input.signal, maxBuffer: 8 * 1024 * 1024,
|
|
274
|
+
detached: Boolean(tree),
|
|
275
|
+
windowsVerbatimArguments: invocation.windowsVerbatimArguments, };
|
|
276
|
+
const child = execFile(invocation.command, invocation.args, options, (error, stdout, stderr) => {
|
|
277
|
+
if (treeTimer)
|
|
278
|
+
clearTimeout(treeTimer);
|
|
279
|
+
input.signal.removeEventListener('abort', killTree);
|
|
280
|
+
if (error)
|
|
281
|
+
killTree();
|
|
282
|
+
const settle = () => { error && !completed ? reject(error) : resolve({ stdout: String(stdout), stderr: String(stderr) }); };
|
|
283
|
+
if (child.pid && child.exitCode === null && child.signalCode === null)
|
|
284
|
+
child.once('close', settle);
|
|
285
|
+
else
|
|
286
|
+
settle();
|
|
294
287
|
});
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
288
|
+
if (tree) {
|
|
289
|
+
input.signal.addEventListener('abort', killTree, { once: true });
|
|
290
|
+
treeTimer = setTimeout(killTree, input.timeoutMs);
|
|
291
|
+
if (input.signal.aborted)
|
|
292
|
+
killTree();
|
|
293
|
+
}
|
|
294
|
+
if (input.complete)
|
|
295
|
+
child.stdout?.on('data', (chunk) => {
|
|
296
|
+
captured += String(chunk);
|
|
297
|
+
if (input.complete?.(captured)) {
|
|
298
|
+
completed = true;
|
|
299
|
+
child.kill('SIGKILL');
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
child.stdin?.on('error', () => { });
|
|
303
|
+
child.stdin?.end(input.stdin);
|
|
304
|
+
}) }, { signal: input.signal });
|
|
298
305
|
}
|
|
299
306
|
export const defaultModelDiscoveryDeps = {
|
|
300
307
|
process: { run: runProcess },
|
|
@@ -319,29 +326,29 @@ export const defaultModelDiscoveryDeps = {
|
|
|
319
326
|
// handles. BYOK env is usable only when it identifies this CLI's effective provider.
|
|
320
327
|
const env = context.env;
|
|
321
328
|
if (agentId === 'claude' && env.ANTHROPIC_API_KEY)
|
|
322
|
-
return { protocol: 'anthropic', baseUrl: env.ANTHROPIC_BASE_URL ||
|
|
329
|
+
return { protocol: 'anthropic', baseUrl: env.ANTHROPIC_BASE_URL || VENDOR_API_BASE_URLS.anthropic, apiKey: env.ANTHROPIC_API_KEY };
|
|
323
330
|
const selected = context.model || (agentId === 'aider' ? env.AIDER_MODEL : agentId === 'qwen' ? env.QWEN_MODEL || env.OPENAI_MODEL : undefined);
|
|
324
331
|
if (!selected)
|
|
325
332
|
return null;
|
|
326
333
|
if (/^(anthropic\/|claude-)/.test(selected) && env.ANTHROPIC_API_KEY)
|
|
327
|
-
return { protocol: 'anthropic', baseUrl: env.ANTHROPIC_BASE_URL ||
|
|
334
|
+
return { protocol: 'anthropic', baseUrl: env.ANTHROPIC_BASE_URL || VENDOR_API_BASE_URLS.anthropic, apiKey: env.ANTHROPIC_API_KEY };
|
|
328
335
|
if (/^(gemini\/|google\/|gemini-)/.test(selected) && env.GEMINI_API_KEY)
|
|
329
|
-
return { protocol: 'google', baseUrl:
|
|
336
|
+
return { protocol: 'google', baseUrl: VENDOR_API_BASE_URLS.google, apiKey: env.GEMINI_API_KEY };
|
|
330
337
|
if (/^openrouter\//.test(selected) && env.OPENROUTER_API_KEY)
|
|
331
|
-
return { protocol: 'openai', baseUrl: env.OPENROUTER_API_BASE ||
|
|
338
|
+
return { protocol: 'openai', baseUrl: env.OPENROUTER_API_BASE || VENDOR_API_BASE_URLS.openrouter, apiKey: env.OPENROUTER_API_KEY };
|
|
332
339
|
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 ||
|
|
340
|
+
return { protocol: 'openai', baseUrl: env.OPENAI_BASE_URL || env.OPENAI_API_BASE || VENDOR_API_BASE_URLS.openai, apiKey: env.OPENAI_API_KEY };
|
|
334
341
|
return null;
|
|
335
342
|
} },
|
|
336
343
|
acp: { async probe({ context, args, signal }) {
|
|
337
344
|
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 });
|
|
345
|
+
const result = await scheduleProbeProcess({ work: () => detectAcpModelMetadata({ bin: invocation.command, args: invocation.args }, { cwd: context.cwd, env: context.env, timeoutMs: 15_000, signal }) }, { signal });
|
|
339
346
|
return { models: result.models, source: 'rpc', coverage: 'account', ...(result.currentModelId ? { defaultSelectionId: result.currentModelId } : {}) };
|
|
340
347
|
} },
|
|
341
348
|
rpc: { async probe({ agentId, context, signal }) {
|
|
342
349
|
// UNVERIFIED locally: use official prompt-free SDK interfaces, not invented CLI flags.
|
|
343
350
|
if (agentId === 'copilot')
|
|
344
|
-
return probeCopilotSdk(context, signal);
|
|
351
|
+
return scheduleProbeProcess({ work: () => probeCopilotSdk(context, signal) }, { signal });
|
|
345
352
|
if (agentId === 'droid')
|
|
346
353
|
return probeDroidSdk(context, defaultModelDiscoveryDeps, signal);
|
|
347
354
|
if (agentId !== 'pi')
|
|
@@ -358,7 +365,7 @@ export const defaultModelDiscoveryDeps = {
|
|
|
358
365
|
} });
|
|
359
366
|
return parsePiRpcMetadata(stdout);
|
|
360
367
|
} },
|
|
361
|
-
clock: { now: Date.now }, cache:
|
|
368
|
+
clock: { now: Date.now }, cache: defaultRuntimeState.modelCatalog,
|
|
362
369
|
};
|
|
363
370
|
/** Pi's native RPC supplies catalog AND current provider/id without making an inference call. */
|
|
364
371
|
export function parsePiRpcMetadata(stdout) {
|
|
@@ -388,4 +395,10 @@ export function parsePiRpcMetadata(stdout) {
|
|
|
388
395
|
throw new ModelProbeError('malformed-response');
|
|
389
396
|
return { models, source: 'rpc', coverage: 'account', ...(defaultSelectionId ? { defaultSelectionId } : {}) };
|
|
390
397
|
}
|
|
398
|
+
/** Stable dependency identity preserves the existing per-host single-probe budget as well as its cache. */
|
|
399
|
+
export function getRuntimeModelDiscoveryDeps({ state }, _optional = {}) {
|
|
400
|
+
if (state === defaultRuntimeState)
|
|
401
|
+
return defaultModelDiscoveryDeps;
|
|
402
|
+
return state.discoveryDeps ??= { ...defaultModelDiscoveryDeps, cache: state.modelCatalog };
|
|
403
|
+
}
|
|
391
404
|
//# sourceMappingURL=model-discovery-adapters.js.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Native composition and legacy public signatures. Injected hosts import model-discovery instead. */
|
|
2
|
+
import { resolveModelForLaunch as resolvePolicyModelForLaunch, type ModelDiscoveryDefinition } from './model-discovery.js';
|
|
3
|
+
import type { AgentDiscoveryConfiguration, AgentModelDiscoveryPort, ModelDiscoveryContext, ModelDiscoveryDeps } from './model-discovery-types.js';
|
|
4
|
+
export * from './model-discovery.js';
|
|
5
|
+
export declare function createAgentModelDiscovery(agentId: string, configuration: AgentDiscoveryConfiguration): AgentModelDiscoveryPort;
|
|
6
|
+
export declare function modelDiscoveryForDef(def: ModelDiscoveryDefinition): AgentModelDiscoveryPort;
|
|
7
|
+
export declare function resolveModelForLaunch(required: {
|
|
8
|
+
def: ModelDiscoveryDefinition;
|
|
9
|
+
context: ModelDiscoveryContext;
|
|
10
|
+
}, { deps, ...optional }?: {
|
|
11
|
+
deps?: ModelDiscoveryDeps;
|
|
12
|
+
signal?: AbortSignal;
|
|
13
|
+
}): ReturnType<typeof resolvePolicyModelForLaunch>;
|
|
14
|
+
//# sourceMappingURL=model-discovery-native.d.ts.map
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** Native composition and legacy public signatures. Injected hosts import model-discovery instead. */
|
|
2
|
+
import { createAgentModelDiscovery as createPolicyDiscovery, modelDiscoveryForDef as policyDiscoveryForDef, resolveModelForLaunch as resolvePolicyModelForLaunch, } from './model-discovery.js';
|
|
3
|
+
import { defaultModelDiscoveryDeps, modelProbeTimeoutMs, probeAgentCatalog, probeCliCache } from './model-discovery-adapters.js';
|
|
4
|
+
export * from './model-discovery.js';
|
|
5
|
+
/** Reuse the existing probe owners, including their credential, cancellation and cache guards. */
|
|
6
|
+
const nativeModelDiscoveryProbes = {
|
|
7
|
+
catalog: ({ agentId, context, configuration, deps, signal }, _optional = {}) => probeAgentCatalog(agentId, context, configuration, deps, signal),
|
|
8
|
+
cliCache: ({ agentId, context, deps, signal }, _optional = {}) => probeCliCache(agentId, context, deps, signal),
|
|
9
|
+
timeoutMs: ({ agentId }, _optional = {}) => modelProbeTimeoutMs(agentId),
|
|
10
|
+
};
|
|
11
|
+
// These forwarding signatures deliberately retain the published API. New policy APIs use
|
|
12
|
+
// required/optional objects; changing these compatibility wrappers would break existing hosts.
|
|
13
|
+
export function createAgentModelDiscovery(agentId, configuration) {
|
|
14
|
+
return createPolicyDiscovery({ agentId, configuration, deps: defaultModelDiscoveryDeps, probes: nativeModelDiscoveryProbes });
|
|
15
|
+
}
|
|
16
|
+
export function modelDiscoveryForDef(def) {
|
|
17
|
+
return policyDiscoveryForDef({ def, deps: defaultModelDiscoveryDeps });
|
|
18
|
+
}
|
|
19
|
+
export function resolveModelForLaunch(required, { deps = defaultModelDiscoveryDeps, ...optional } = {}) {
|
|
20
|
+
return resolvePolicyModelForLaunch({ ...required, deps }, optional);
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=model-discovery-native.js.map
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ModelProbeError, modelIdentityKind } from './model-discovery.js';
|
|
1
|
+
import { ModelProbeError, modelIdentityKind } from './model-discovery-shared.js';
|
|
2
2
|
export async function probeCopilotSdk(context, signal, optional = {}) {
|
|
3
3
|
const client = await (optional.createClient ?? createCopilotMetadataClient)(context);
|
|
4
4
|
if (signal.aborted) {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Shared discovery normalization and timeout/error contracts; no native adapters. */
|
|
2
|
+
import type { RuntimeModelOption } from './types.js';
|
|
3
|
+
import type { DiscoveredModel, ModelCatalogSnapshot } from './model-discovery-types.js';
|
|
4
|
+
export declare function modelIdentityKind(id: string): DiscoveredModel['identityKind'];
|
|
5
|
+
export declare function normalizeDiscoveredModels(models: readonly RuntimeModelOption[]): DiscoveredModel[];
|
|
6
|
+
export declare class ProbeTimeout extends Error {
|
|
7
|
+
}
|
|
8
|
+
/** Cancellation always reaches the effect port. A broken port cannot hold callers indefinitely. */
|
|
9
|
+
export declare function withModelProbeTimeout<T>(work: (signal: AbortSignal) => Promise<T>, timeoutMs: number, parent?: AbortSignal): Promise<T>;
|
|
10
|
+
export declare class ModelProbeError extends Error {
|
|
11
|
+
readonly code: ModelCatalogSnapshot['diagnostics'][number]['code'];
|
|
12
|
+
constructor(code: ModelCatalogSnapshot['diagnostics'][number]['code']);
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=model-discovery-shared.d.ts.map
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export function modelIdentityKind(id) {
|
|
2
|
+
const token = id.slice(id.lastIndexOf('/') + 1);
|
|
3
|
+
if (/^(default|auto|adaptive|smart|deep|rush|ultimate|premium|balanced|fast|vercel-ai-gateway|grok-build)$/i.test(token) || /(?:^|[-/])gateway$/i.test(id))
|
|
4
|
+
return 'routing-mode';
|
|
5
|
+
if (/^(opus|sonnet|haiku|fable|best|latest|claude|gemini|gpt|codex|swe)(?:\[.*\])?$/i.test(token) || /(?:^|[-/])latest$/i.test(id))
|
|
6
|
+
return 'alias';
|
|
7
|
+
return 'concrete';
|
|
8
|
+
}
|
|
9
|
+
export function normalizeDiscoveredModels(models) {
|
|
10
|
+
const seen = new Set();
|
|
11
|
+
return models.flatMap((row) => {
|
|
12
|
+
if (!row.id || row.id === 'default' || seen.has(row.id))
|
|
13
|
+
return [];
|
|
14
|
+
seen.add(row.id);
|
|
15
|
+
const model = row;
|
|
16
|
+
return [{ ...row, identityKind: model.identityKind ?? modelIdentityKind(row.id), ...(model.resolvedId ? { resolvedId: model.resolvedId } : {}) }];
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
export class ProbeTimeout extends Error {
|
|
20
|
+
}
|
|
21
|
+
/** Cancellation always reaches the effect port. A broken port cannot hold callers indefinitely. */
|
|
22
|
+
export async function withModelProbeTimeout(work, timeoutMs, parent) {
|
|
23
|
+
if (parent?.aborted)
|
|
24
|
+
throw new ProbeTimeout('Probe cancelled');
|
|
25
|
+
const controller = new AbortController();
|
|
26
|
+
let timer;
|
|
27
|
+
let abortListener;
|
|
28
|
+
const stopped = new Promise((_resolve, reject) => {
|
|
29
|
+
abortListener = () => { controller.abort(); reject(new ProbeTimeout('Probe cancelled')); };
|
|
30
|
+
if (parent?.aborted)
|
|
31
|
+
abortListener();
|
|
32
|
+
else
|
|
33
|
+
parent?.addEventListener('abort', abortListener, { once: true });
|
|
34
|
+
timer = setTimeout(() => { controller.abort(); reject(new ProbeTimeout('Probe timed out')); }, timeoutMs);
|
|
35
|
+
});
|
|
36
|
+
try {
|
|
37
|
+
return await Promise.race([work(controller.signal), stopped]);
|
|
38
|
+
}
|
|
39
|
+
finally {
|
|
40
|
+
if (timer)
|
|
41
|
+
clearTimeout(timer);
|
|
42
|
+
if (abortListener)
|
|
43
|
+
parent?.removeEventListener('abort', abortListener);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export class ModelProbeError extends Error {
|
|
47
|
+
code;
|
|
48
|
+
constructor(code) {
|
|
49
|
+
super(code);
|
|
50
|
+
this.code = code;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=model-discovery-shared.js.map
|
|
@@ -136,4 +136,30 @@ export interface ModelDiscoveryDeps {
|
|
|
136
136
|
};
|
|
137
137
|
cache: Map<string, ModelCacheEntry>;
|
|
138
138
|
}
|
|
139
|
+
export interface AgentDiscoveryConfiguration {
|
|
140
|
+
fallbackModels: () => readonly RuntimeModelOption[];
|
|
141
|
+
parse?: (input: {
|
|
142
|
+
stdout: string;
|
|
143
|
+
}) => RuntimeModelOption[] | null;
|
|
144
|
+
args?: readonly string[];
|
|
145
|
+
}
|
|
146
|
+
/** Host composition selects native probes or substitutes another discovery mechanism. */
|
|
147
|
+
export interface ModelDiscoveryProbes {
|
|
148
|
+
catalog(required: {
|
|
149
|
+
agentId: string;
|
|
150
|
+
context: ModelDiscoveryContext;
|
|
151
|
+
configuration: AgentDiscoveryConfiguration;
|
|
152
|
+
deps: ModelDiscoveryDeps;
|
|
153
|
+
signal: AbortSignal;
|
|
154
|
+
}, optional?: Record<string, never>): Promise<ModelProbeResult>;
|
|
155
|
+
cliCache(required: {
|
|
156
|
+
agentId: string;
|
|
157
|
+
context: ModelDiscoveryContext;
|
|
158
|
+
deps: ModelDiscoveryDeps;
|
|
159
|
+
signal: AbortSignal;
|
|
160
|
+
}, optional?: Record<string, never>): Promise<ModelProbeResult>;
|
|
161
|
+
timeoutMs(required: {
|
|
162
|
+
agentId: string;
|
|
163
|
+
}, optional?: Record<string, never>): number;
|
|
164
|
+
}
|
|
139
165
|
//# sourceMappingURL=model-discovery-types.d.ts.map
|
|
@@ -1,42 +1,37 @@
|
|
|
1
1
|
import type { RuntimeModelOption } from './types.js';
|
|
2
|
-
import type { AgentModelDiscoveryPort,
|
|
3
|
-
export
|
|
2
|
+
import type { AgentModelDiscoveryPort, AgentDiscoveryConfiguration, DefaultModelResolution, ModelCatalogSnapshot, ModelDiscoveryContext, ModelDiscoveryDeps, ModelDiscoveryProbes } from './model-discovery-types.js';
|
|
3
|
+
export { ModelProbeError, modelIdentityKind, normalizeDiscoveredModels, withModelProbeTimeout } from './model-discovery-shared.js';
|
|
4
|
+
export type * from './model-discovery-types.js';
|
|
4
5
|
export declare const MODEL_CATALOG_TTL_MS: number;
|
|
5
6
|
export declare const MODEL_NEGATIVE_TTL_MS = 30000;
|
|
6
|
-
export declare function modelIdentityKind(id: string): DiscoveredModel['identityKind'];
|
|
7
|
-
export declare function normalizeDiscoveredModels(models: readonly RuntimeModelOption[]): DiscoveredModel[];
|
|
8
7
|
/** Hash all launch-affecting state, including env credentials; none of the raw material escapes. */
|
|
9
8
|
export declare function modelLaunchFingerprint(context: ModelDiscoveryContext, configMaterial?: string): string;
|
|
10
|
-
/**
|
|
11
|
-
export declare function
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
fallbackModels: () => readonly RuntimeModelOption[];
|
|
18
|
-
parse?: (input: {
|
|
19
|
-
stdout: string;
|
|
20
|
-
}) => RuntimeModelOption[] | null;
|
|
21
|
-
args?: readonly string[];
|
|
22
|
-
}
|
|
23
|
-
export declare function createAgentModelDiscovery(agentId: string, configuration: AgentDiscoveryConfiguration): AgentModelDiscoveryPort;
|
|
9
|
+
/** Bind supplied effect ports and probes without loading their native implementations. */
|
|
10
|
+
export declare function createAgentModelDiscovery({ agentId, configuration, deps: suppliedDeps, probes }: {
|
|
11
|
+
agentId: string;
|
|
12
|
+
configuration: AgentDiscoveryConfiguration;
|
|
13
|
+
deps: ModelDiscoveryDeps;
|
|
14
|
+
probes: ModelDiscoveryProbes;
|
|
15
|
+
}, _optional?: Record<string, never>): AgentModelDiscoveryPort;
|
|
24
16
|
export declare function offlineSnapshot(fingerprint: string, now: number, fallback: readonly RuntimeModelOption[], issue?: ModelCatalogSnapshot['diagnostics'][number]): ModelCatalogSnapshot;
|
|
25
17
|
/** Hosts may supply either discovery port independently; static definitions need no probes. */
|
|
26
|
-
type ModelDiscoveryDefinition = Partial<AgentModelDiscoveryPort> & {
|
|
18
|
+
export type ModelDiscoveryDefinition = Partial<AgentModelDiscoveryPort> & {
|
|
27
19
|
id?: string;
|
|
28
20
|
fallbackModels?: readonly RuntimeModelOption[];
|
|
29
21
|
defaultModelEnvVar?: string;
|
|
30
22
|
supportsCustomModel?: boolean;
|
|
31
23
|
supportsConcreteModelSelection?: boolean;
|
|
32
24
|
};
|
|
33
|
-
export declare function modelDiscoveryForDef(def:
|
|
25
|
+
export declare function modelDiscoveryForDef({ def, deps: suppliedDeps }: {
|
|
26
|
+
def: ModelDiscoveryDefinition;
|
|
27
|
+
deps: ModelDiscoveryDeps;
|
|
28
|
+
}, _optional?: Record<string, never>): AgentModelDiscoveryPort;
|
|
34
29
|
/** Resolve against the actual launch scope, pinning the same ID shown by the picker. */
|
|
35
|
-
export declare function resolveModelForLaunch({ def, context }: {
|
|
30
|
+
export declare function resolveModelForLaunch({ def, context, deps }: {
|
|
36
31
|
def: ModelDiscoveryDefinition;
|
|
37
32
|
context: ModelDiscoveryContext;
|
|
33
|
+
deps: ModelDiscoveryDeps;
|
|
38
34
|
}, optional?: {
|
|
39
|
-
deps?: ModelDiscoveryDeps;
|
|
40
35
|
signal?: AbortSignal;
|
|
41
36
|
}): Promise<{
|
|
42
37
|
model: string;
|
package/dist/model-discovery.js
CHANGED
|
@@ -1,40 +1,20 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { readModelConfiguration } from './model-discovery-config.js';
|
|
3
|
-
import {
|
|
4
|
-
export
|
|
3
|
+
import { ModelProbeError, ProbeTimeout, modelIdentityKind, normalizeDiscoveredModels, withModelProbeTimeout } from './model-discovery-shared.js';
|
|
4
|
+
export { ModelProbeError, modelIdentityKind, normalizeDiscoveredModels, withModelProbeTimeout } from './model-discovery-shared.js';
|
|
5
5
|
export const MODEL_CATALOG_TTL_MS = 15 * 60_000;
|
|
6
6
|
export const MODEL_NEGATIVE_TTL_MS = 30_000;
|
|
7
7
|
const MAX_CATALOG_SCOPES = 128;
|
|
8
|
-
export function modelIdentityKind(id) {
|
|
9
|
-
const token = id.slice(id.lastIndexOf('/') + 1);
|
|
10
|
-
if (/^(default|auto|adaptive|smart|deep|rush|ultimate|premium|balanced|fast|vercel-ai-gateway|grok-build)$/i.test(token) || /(?:^|[-/])gateway$/i.test(id))
|
|
11
|
-
return 'routing-mode';
|
|
12
|
-
if (/^(opus|sonnet|haiku|fable|best|latest|claude|gemini|gpt|codex|swe)(?:\[.*\])?$/i.test(token) || /(?:^|[-/])latest$/i.test(id))
|
|
13
|
-
return 'alias';
|
|
14
|
-
return 'concrete';
|
|
15
|
-
}
|
|
16
|
-
export function normalizeDiscoveredModels(models) {
|
|
17
|
-
const seen = new Set();
|
|
18
|
-
return models.flatMap((row) => {
|
|
19
|
-
if (!row.id || row.id === 'default' || seen.has(row.id))
|
|
20
|
-
return [];
|
|
21
|
-
seen.add(row.id);
|
|
22
|
-
const model = row;
|
|
23
|
-
return [{ ...row, identityKind: model.identityKind ?? modelIdentityKind(row.id), ...(model.resolvedId ? { resolvedId: model.resolvedId } : {}) }];
|
|
24
|
-
});
|
|
25
|
-
}
|
|
26
8
|
/** Hash all launch-affecting state, including env credentials; none of the raw material escapes. */
|
|
27
9
|
export function modelLaunchFingerprint(context, configMaterial = '') {
|
|
28
10
|
return createHash('sha256').update(JSON.stringify({ ...context, env: Object.entries(context.env).sort(([a], [b]) => a.localeCompare(b)), configMaterial })).digest('hex');
|
|
29
11
|
}
|
|
30
|
-
class ProbeTimeout extends Error {
|
|
31
|
-
}
|
|
32
12
|
const probePools = new WeakMap();
|
|
33
|
-
/**
|
|
13
|
+
/** One cold catalog probe per host; queued callers keep cancellation responsive. */
|
|
34
14
|
async function boundedProbe(deps, work, signal) {
|
|
35
15
|
const pool = probePools.get(deps) || { active: 0, queue: [] };
|
|
36
16
|
probePools.set(deps, pool);
|
|
37
|
-
if (pool.active >=
|
|
17
|
+
if (pool.active >= 1) {
|
|
38
18
|
await new Promise((resolve, reject) => {
|
|
39
19
|
const ready = () => { signal?.removeEventListener('abort', abort); resolve(); };
|
|
40
20
|
const abort = () => { const index = pool.queue.indexOf(ready); if (index >= 0)
|
|
@@ -61,50 +41,19 @@ async function boundedProbe(deps, work, signal) {
|
|
|
61
41
|
pool.active -= 1;
|
|
62
42
|
}
|
|
63
43
|
}
|
|
64
|
-
/** Cancellation always reaches the effect port. A broken port cannot hold callers indefinitely. */
|
|
65
|
-
export async function withModelProbeTimeout(work, timeoutMs, parent) {
|
|
66
|
-
if (parent?.aborted)
|
|
67
|
-
throw new ProbeTimeout('Probe cancelled');
|
|
68
|
-
const controller = new AbortController();
|
|
69
|
-
let timer;
|
|
70
|
-
let abortListener;
|
|
71
|
-
const stopped = new Promise((_resolve, reject) => {
|
|
72
|
-
abortListener = () => { controller.abort(); reject(new ProbeTimeout('Probe cancelled')); };
|
|
73
|
-
if (parent?.aborted)
|
|
74
|
-
abortListener();
|
|
75
|
-
else
|
|
76
|
-
parent?.addEventListener('abort', abortListener, { once: true });
|
|
77
|
-
timer = setTimeout(() => { controller.abort(); reject(new ProbeTimeout('Probe timed out')); }, timeoutMs);
|
|
78
|
-
});
|
|
79
|
-
try {
|
|
80
|
-
return await Promise.race([work(controller.signal), stopped]);
|
|
81
|
-
}
|
|
82
|
-
finally {
|
|
83
|
-
if (timer)
|
|
84
|
-
clearTimeout(timer);
|
|
85
|
-
if (abortListener)
|
|
86
|
-
parent?.removeEventListener('abort', abortListener);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
44
|
function diagnostic(error) {
|
|
90
45
|
// Never stringify vendor errors: stderr/HTTP bodies may include credentials or OAuth URLs.
|
|
91
46
|
const code = error instanceof ProbeTimeout ? 'timeout' : error instanceof ModelProbeError ? error.code : 'offline';
|
|
92
47
|
return { code, message: ({ timeout: 'Model discovery timed out or was cancelled.', 'auth-missing': 'CLI credentials are unavailable.', 'unsupported-version': 'Native metadata discovery is unavailable for this version.', 'malformed-response': 'Native metadata contained no usable catalog.', offline: 'Model discovery could not reach its source.' })[code] };
|
|
93
48
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
constructor(code) {
|
|
97
|
-
super(code);
|
|
98
|
-
this.code = code;
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
export function createAgentModelDiscovery(agentId, configuration) {
|
|
49
|
+
/** Bind supplied effect ports and probes without loading their native implementations. */
|
|
50
|
+
export function createAgentModelDiscovery({ agentId, configuration, deps: suppliedDeps, probes }, _optional = {}) {
|
|
102
51
|
async function scoped(context, deps, signal) {
|
|
103
52
|
const config = await withModelProbeTimeout((active) => readModelConfiguration(agentId, context, deps, active), 1000, signal);
|
|
104
53
|
return { config, key: `${agentId}:${modelLaunchFingerprint(context, config.fingerprintMaterial)}` };
|
|
105
54
|
}
|
|
106
55
|
const port = {
|
|
107
|
-
async discoverModels({ context }, { signal, force = false, staleWhileRevalidate = false, deps =
|
|
56
|
+
async discoverModels({ context }, { signal, force = false, staleWhileRevalidate = false, deps = suppliedDeps } = {}) {
|
|
108
57
|
let state;
|
|
109
58
|
try {
|
|
110
59
|
state = await scoped(context, deps, signal);
|
|
@@ -129,7 +78,7 @@ export function createAgentModelDiscovery(agentId, configuration) {
|
|
|
129
78
|
// immediately; keep stale provenance and refresh without holding the first user prompt.
|
|
130
79
|
if (staleWhileRevalidate && !cached.good && ['codex', 'opencode', 'claude', 'codebuddy'].includes(agentId)) {
|
|
131
80
|
try {
|
|
132
|
-
const disk = await withModelProbeTimeout(active =>
|
|
81
|
+
const disk = await withModelProbeTimeout(active => probes.cliCache({ agentId, context, deps, signal: active }), 1000, signal);
|
|
133
82
|
const models = normalizeDiscoveredModels(disk.models);
|
|
134
83
|
if (models.length)
|
|
135
84
|
cached.good = { models, source: 'cli-cache', coverage: disk.coverage,
|
|
@@ -145,7 +94,7 @@ export function createAgentModelDiscovery(agentId, configuration) {
|
|
|
145
94
|
}
|
|
146
95
|
const pending = (async () => {
|
|
147
96
|
try {
|
|
148
|
-
const result = await boundedProbe(deps, () => withModelProbeTimeout((active) =>
|
|
97
|
+
const result = await boundedProbe(deps, () => withModelProbeTimeout((active) => probes.catalog({ agentId, context, configuration, deps, signal: active }), probes.timeoutMs({ agentId }), signal), signal);
|
|
149
98
|
const models = normalizeDiscoveredModels(result.models);
|
|
150
99
|
if (models.length === 0)
|
|
151
100
|
throw new ModelProbeError('malformed-response');
|
|
@@ -184,7 +133,7 @@ export function createAgentModelDiscovery(agentId, configuration) {
|
|
|
184
133
|
// catalog while this coalesced, timeout-bounded refresh updates the cache in the background.
|
|
185
134
|
return staleWhileRevalidate && lastGood() || pending;
|
|
186
135
|
},
|
|
187
|
-
async resolveDefaultModel({ context, catalog }, { signal, deps =
|
|
136
|
+
async resolveDefaultModel({ context, catalog }, { signal, deps = suppliedDeps } = {}) {
|
|
188
137
|
let state;
|
|
189
138
|
try {
|
|
190
139
|
state = await scoped(context, deps, signal);
|
|
@@ -214,9 +163,9 @@ export function createAgentModelDiscovery(agentId, configuration) {
|
|
|
214
163
|
export function offlineSnapshot(fingerprint, now, fallback, issue) {
|
|
215
164
|
return { models: normalizeDiscoveredModels(fallback), source: 'offline-fallback', freshness: 'offline-fallback', fetchedAt: new Date(now).toISOString(), expiresAt: new Date(now + MODEL_NEGATIVE_TTL_MS).toISOString(), coverage: 'configured', launchFingerprint: fingerprint, diagnostics: issue ? [issue] : [] };
|
|
216
165
|
}
|
|
217
|
-
export function modelDiscoveryForDef(def) {
|
|
166
|
+
export function modelDiscoveryForDef({ def, deps: suppliedDeps }, _optional = {}) {
|
|
218
167
|
return {
|
|
219
|
-
discoverModels: def.discoverModels?.bind(def) || (async ({ context }, { deps =
|
|
168
|
+
discoverModels: def.discoverModels?.bind(def) || (async ({ context }, { deps = suppliedDeps } = {}) => {
|
|
220
169
|
const catalog = offlineSnapshot(modelLaunchFingerprint(context), deps.clock.now(), def.fallbackModels || []);
|
|
221
170
|
const configured = def.defaultModelEnvVar && context.env[def.defaultModelEnvVar]?.trim();
|
|
222
171
|
// Static hosts used their first concrete fallback before discovery existed. Preserve
|
|
@@ -224,7 +173,7 @@ export function modelDiscoveryForDef(def) {
|
|
|
224
173
|
const selection = context.model || context.resumedModelId || configured || catalog.models.find(row => row.identityKind === 'concrete')?.id;
|
|
225
174
|
return { ...catalog, ...(selection ? { defaultSelectionId: selection } : {}) };
|
|
226
175
|
}),
|
|
227
|
-
resolveDefaultModel: def.resolveDefaultModel?.bind(def) || (async ({ context, catalog }, { deps =
|
|
176
|
+
resolveDefaultModel: def.resolveDefaultModel?.bind(def) || (async ({ context, catalog }, { deps = suppliedDeps } = {}) => {
|
|
228
177
|
if (context.resumeSessionId && !context.resumedModelId && !context.model)
|
|
229
178
|
return { status: 'unresolved', reason: 'Resumed session model metadata is required.' };
|
|
230
179
|
const configured = def.defaultModelEnvVar && context.env[def.defaultModelEnvVar]?.trim();
|
|
@@ -241,16 +190,16 @@ export function modelDiscoveryForDef(def) {
|
|
|
241
190
|
};
|
|
242
191
|
}
|
|
243
192
|
/** Resolve against the actual launch scope, pinning the same ID shown by the picker. */
|
|
244
|
-
export async function resolveModelForLaunch({ def, context }, optional = {}) {
|
|
193
|
+
export async function resolveModelForLaunch({ def, context, deps }, optional = {}) {
|
|
245
194
|
if (def.supportsConcreteModelSelection === false)
|
|
246
195
|
throw new Error('This CLI cannot pin a concrete starting model.');
|
|
247
|
-
const discovery = modelDiscoveryForDef(def);
|
|
248
|
-
const catalog = await discovery.discoverModels({ context }, { ...optional, staleWhileRevalidate: true });
|
|
196
|
+
const discovery = modelDiscoveryForDef({ def, deps });
|
|
197
|
+
const catalog = await discovery.discoverModels({ context }, { ...optional, deps, staleWhileRevalidate: true });
|
|
249
198
|
// Custom input is allowed unless the definition explicitly disables it: an unlisted user ID is already
|
|
250
199
|
// the intended launch value. Never substitute a catalog default or reinterpret its spelling.
|
|
251
200
|
const resolution = context.model && def.supportsCustomModel !== false && !catalog.models.some(row => row.id === context.model)
|
|
252
|
-
? { status: 'resolved', id: context.model, selectionId: context.model, source: 'config-file', resolvedAt: new Date(
|
|
253
|
-
: await discovery.resolveDefaultModel({ context, catalog }, optional);
|
|
201
|
+
? { status: 'resolved', id: context.model, selectionId: context.model, source: 'config-file', resolvedAt: new Date(deps.clock.now()).toISOString(), launchFingerprint: catalog.launchFingerprint }
|
|
202
|
+
: await discovery.resolveDefaultModel({ context, catalog }, { ...optional, deps });
|
|
254
203
|
if (resolution.status !== 'resolved')
|
|
255
204
|
throw new Error(resolution.reason);
|
|
256
205
|
return { model: resolution.id, catalog, resolution };
|
package/dist/models.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type AgentRuntimeState } from './runtime-state.js';
|
|
1
2
|
/**
|
|
2
3
|
* @module models
|
|
3
4
|
* Model-option defaults, a per-agent last-surfaced live-model cache and model-id sanitization.
|
|
@@ -7,13 +8,15 @@ export declare const DEFAULT_MODEL_OPTION: RuntimeModelOption;
|
|
|
7
8
|
export declare function rememberLiveModels({ agentId, models }: {
|
|
8
9
|
agentId: string;
|
|
9
10
|
models: RuntimeModelOption[];
|
|
10
|
-
}, { scope }?: {
|
|
11
|
+
}, { scope, state }?: {
|
|
11
12
|
scope?: string | null;
|
|
13
|
+
state?: AgentRuntimeState;
|
|
12
14
|
}): void;
|
|
13
15
|
export declare function getRememberedLiveModels({ agentId }: {
|
|
14
16
|
agentId: string;
|
|
15
|
-
}, { scope }?: {
|
|
17
|
+
}, { scope, state }?: {
|
|
16
18
|
scope?: string | null;
|
|
19
|
+
state?: AgentRuntimeState;
|
|
17
20
|
}): RuntimeModelOption[];
|
|
18
21
|
export declare function preferFreshLiveModels({ freshModels, rememberedModels }: {
|
|
19
22
|
freshModels: RuntimeModelOption[];
|
|
@@ -22,15 +25,17 @@ export declare function preferFreshLiveModels({ freshModels, rememberedModels }:
|
|
|
22
25
|
export declare function isKnownModel({ def, modelId }: {
|
|
23
26
|
def: RuntimeAgentDef;
|
|
24
27
|
modelId: string | null | undefined;
|
|
25
|
-
}, { scope }?: {
|
|
28
|
+
}, { scope, state }?: {
|
|
26
29
|
scope?: string | null;
|
|
30
|
+
state?: AgentRuntimeState;
|
|
27
31
|
}): boolean;
|
|
28
32
|
export declare function resolveModelForAgent({ def, resolved }: {
|
|
29
33
|
def: RuntimeAgentDef;
|
|
30
34
|
resolved: string | null;
|
|
31
|
-
}, { env, liveModelScope }?: {
|
|
35
|
+
}, { env, liveModelScope, state }?: {
|
|
32
36
|
env?: Record<string, string | undefined>;
|
|
33
37
|
liveModelScope?: string | null;
|
|
38
|
+
state?: AgentRuntimeState;
|
|
34
39
|
}): string | null;
|
|
35
40
|
export declare function sanitizeCustomModel({ id }: {
|
|
36
41
|
id: string | null | undefined;
|