amicus 2.2.0 → 3.1.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/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +80 -0
- package/README.md +13 -8
- package/bin/amicus.js +5 -0
- package/electron/close-guard.js +4 -4
- package/electron/fold.js +8 -8
- package/electron/ipc-guard.js +3 -3
- package/electron/main.js +31 -31
- package/electron/opencode-theme.js +3 -3
- package/electron/preload-content.js +1 -1
- package/electron/setup-ui.js +27 -3
- package/package.json +4 -3
- package/skills/second-opinion/SKILL.md +2 -0
- package/skills/sidecar/SKILL.md +66 -38
- package/src/cli-handlers-resume-continue.js +31 -4
- package/src/cli-handlers-run.js +15 -4
- package/src/cli.js +13 -0
- package/src/mcp-server.js +99 -12
- package/src/mcp-tools.js +26 -4
- package/src/opencode-client.js +18 -2
- package/src/sidecar/continue.js +10 -3
- package/src/sidecar/electron-install.js +9 -8
- package/src/sidecar/fanout-leg.js +26 -1
- package/src/sidecar/fanout-output.js +5 -0
- package/src/sidecar/fanout-validate.js +81 -0
- package/src/sidecar/fanout.js +65 -77
- package/src/sidecar/session-utils.js +6 -0
- package/src/sidecar/setup.js +2 -1
- package/src/utils/alias-resolver.js +6 -35
- package/src/utils/api-key-store.js +1 -9
- package/src/utils/auth-json.js +1 -1
- package/src/utils/config.js +98 -16
- package/src/utils/curated-models.js +33 -4
- package/src/utils/gateway-router.js +115 -0
- package/src/utils/input-validators.js +12 -42
- package/src/utils/model-classification.js +65 -0
- package/src/utils/model-descriptor.js +72 -0
- package/src/utils/model-fetcher.js +35 -9
- package/src/utils/model-input-default.js +32 -0
- package/src/utils/model-validator.js +68 -84
- package/src/utils/node-version-guard.js +16 -0
- package/src/utils/provider-registry.js +57 -0
- package/src/utils/quick-picks.js +11 -3
- package/src/utils/result-schema-rebuild.js +98 -0
- package/src/utils/result-schema.js +6 -76
- package/src/utils/route-error.js +137 -0
- package/src/utils/route-launch.js +179 -0
- package/src/utils/start-helpers.js +96 -43
- package/src/utils/validators.js +1 -8
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Route-launch views (#61 gateway routing integration, Task 4.2).
|
|
3
|
+
*
|
|
4
|
+
* Additive, read-only helpers consumed by Task 4.4's resolveRouteForLaunch
|
|
5
|
+
* (not wired into any launch path yet). Pure-ish: all I/O goes through the
|
|
6
|
+
* stubbable api-key-store / auth-json / model-catalog modules.
|
|
7
|
+
*/
|
|
8
|
+
'use strict';
|
|
9
|
+
|
|
10
|
+
const { readApiKeys } = require('./api-key-store');
|
|
11
|
+
const { readAuthJsonKeys } = require('./auth-json');
|
|
12
|
+
const { KNOWN_PROVIDERS } = require('./provider-registry');
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Per-provider key presence across BOTH sources: env/.env (readApiKeys) and
|
|
16
|
+
* OpenCode's auth.json (readAuthJsonKeys). True if either source has a key
|
|
17
|
+
* for that provider (Foundation carry-forward, Decision 5).
|
|
18
|
+
* @returns {Object<string,boolean>} map of provider id -> key present
|
|
19
|
+
*/
|
|
20
|
+
function buildLaunchKeys() {
|
|
21
|
+
const env = readApiKeys(); // {openrouter:bool, google:bool, openai:bool, anthropic:bool, deepseek:bool}
|
|
22
|
+
const authKeys = readAuthJsonKeys(); // {provider:string,...} (only providers with keys)
|
|
23
|
+
const out = {};
|
|
24
|
+
for (const p of KNOWN_PROVIDERS) {
|
|
25
|
+
out[p] = !!env[p] || !!authKeys[p];
|
|
26
|
+
}
|
|
27
|
+
return out;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Thin wrapper over model-catalog.getCatalogInfo() for route-resolution
|
|
32
|
+
* callers that only need the models list and the last-refresh error, not the
|
|
33
|
+
* full cache metadata. Never throws: a catalog error resolves to an empty
|
|
34
|
+
* list with a sentinel error string.
|
|
35
|
+
* @returns {Promise<{models: Array, lastRefreshError: string|null}>}
|
|
36
|
+
*/
|
|
37
|
+
async function getRouteCatalogInfo() {
|
|
38
|
+
// Lazy-required so jest.doMock('./model-catalog', ...) can intercept it
|
|
39
|
+
// per-test, matching the pattern model-catalog.js itself uses for its deps.
|
|
40
|
+
const { getCatalogInfo } = require('./model-catalog');
|
|
41
|
+
try {
|
|
42
|
+
const info = await getCatalogInfo();
|
|
43
|
+
return { models: info.models || [], lastRefreshError: info.lastRefreshError || null };
|
|
44
|
+
} catch {
|
|
45
|
+
return { models: [], lastRefreshError: 'catalog-unavailable' };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Module version stamped onto `resolved` results' provenance (carry-forward). */
|
|
50
|
+
const ROUTE_VERSION = 1;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Build up to ~6 labeled alternatives for a `selection_required` RouteResult
|
|
54
|
+
* (#61 Task 6.3, spec Decision 10). Pure: reads only the already-parsed
|
|
55
|
+
* descriptor plus the live keys/catalogInfo the caller already assembled.
|
|
56
|
+
*
|
|
57
|
+
* Two categories, in order:
|
|
58
|
+
* 1. The SAME model via OpenRouter — only when an OpenRouter key is present
|
|
59
|
+
* AND the OR-namespaced id (`openrouter/<vendor>/<model>`) is actually
|
|
60
|
+
* present in the catalog (never suggest an id we can't confirm exists).
|
|
61
|
+
* 2. Up to 5 OTHER models in the same direct vendor namespace (ids starting
|
|
62
|
+
* `<vendor>/`, excluding the requested id itself and excluding any
|
|
63
|
+
* `openrouter/`-prefixed rows, which share the `<vendor>/` prefix check
|
|
64
|
+
* only when vendor === 'openrouter' and are filtered out defensively).
|
|
65
|
+
*
|
|
66
|
+
* @param {{vendor?: string, model?: string}} descriptor parsed Descriptor for
|
|
67
|
+
* the request that produced the selection_required (canonical or
|
|
68
|
+
* openrouter-literal — both carry vendor/model)
|
|
69
|
+
* @param {Object<string,boolean>} keys per-provider key-presence map (buildLaunchKeys() shape)
|
|
70
|
+
* @param {{models: Array<{id:string}>}} catalogInfo
|
|
71
|
+
* @returns {Array<{model:string, gateway:string, note:string}>}
|
|
72
|
+
*/
|
|
73
|
+
function buildSuggestions(descriptor, keys, catalogInfo) {
|
|
74
|
+
const suggestions = [];
|
|
75
|
+
const vendor = descriptor && descriptor.vendor;
|
|
76
|
+
const model = descriptor && descriptor.model;
|
|
77
|
+
if (!vendor || !model) { return suggestions; }
|
|
78
|
+
|
|
79
|
+
const models = (catalogInfo && Array.isArray(catalogInfo.models)) ? catalogInfo.models : [];
|
|
80
|
+
const requestedDirectId = `${vendor}/${model}`;
|
|
81
|
+
|
|
82
|
+
if (keys && keys.openrouter) {
|
|
83
|
+
const orId = `openrouter/${vendor}/${model}`;
|
|
84
|
+
if (models.some(m => m && m.id === orId)) {
|
|
85
|
+
suggestions.push({ model: orId, gateway: 'openrouter', note: 'same model via OpenRouter' });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const nsPrefix = `${vendor}/`;
|
|
90
|
+
const sameVendor = models.filter(m =>
|
|
91
|
+
m && typeof m.id === 'string' &&
|
|
92
|
+
m.id.startsWith(nsPrefix) &&
|
|
93
|
+
!m.id.startsWith('openrouter/') &&
|
|
94
|
+
m.id !== requestedDirectId
|
|
95
|
+
).slice(0, 5);
|
|
96
|
+
for (const m of sameVendor) {
|
|
97
|
+
suggestions.push({ model: m.id, gateway: 'direct', note: `${vendor} model` });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return suggestions.slice(0, 6);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* One-time per-vendor notice when auto-routing migrates a both-key holder off
|
|
105
|
+
* OpenRouter onto direct (#61 Task 5.1 — visible-migration guarantee: never
|
|
106
|
+
* silent). Advisory only — never changes the routing decision, and a failed
|
|
107
|
+
* persist (markMigrationNotified is itself best-effort) never blocks the
|
|
108
|
+
* launch. Fires only when ALL of these hold:
|
|
109
|
+
* - the result actually resolved to gateway 'direct'
|
|
110
|
+
* - the caller did NOT explicitly force a gateway (gatewayMode === 'auto');
|
|
111
|
+
* an explicit --gateway direct means the user chose direct, no notice
|
|
112
|
+
* - the descriptor is not itself an explicit `openrouter/...` literal
|
|
113
|
+
* - the user holds an OpenRouter key (otherwise nothing is being migrated
|
|
114
|
+
* FROM)
|
|
115
|
+
* - this vendor hasn't already been notified (getRoutingConfig().migration_notified)
|
|
116
|
+
* @param {{result:object, descriptor:object, gatewayMode:string, keys:object}} args
|
|
117
|
+
* @returns {object} the (possibly mutated) result
|
|
118
|
+
*/
|
|
119
|
+
function maybeMigrationNotice({ result, descriptor, gatewayMode, keys }) {
|
|
120
|
+
if (result.kind !== 'resolved' || result.gateway !== 'direct') { return result; }
|
|
121
|
+
if (gatewayMode !== 'auto') { return result; }
|
|
122
|
+
if (descriptor.isExplicitOpenRouter) { return result; }
|
|
123
|
+
if (!keys.openrouter) { return result; }
|
|
124
|
+
try {
|
|
125
|
+
// Lazy-required so jest.doMock('./config', ...) can intercept it per-test.
|
|
126
|
+
const { getRoutingConfig, markMigrationNotified } = require('./config');
|
|
127
|
+
if (getRoutingConfig().migration_notified[descriptor.vendor]) { return result; }
|
|
128
|
+
const notice = `Routing ${descriptor.vendor} via direct API (previously OpenRouter). ` +
|
|
129
|
+
'Set routing.prefer: "openrouter" (or use --gateway openrouter) to restore.';
|
|
130
|
+
result.notice = result.notice ? `${result.notice} ${notice}` : notice;
|
|
131
|
+
markMigrationNotified(descriptor.vendor);
|
|
132
|
+
} catch (_err) {
|
|
133
|
+
// Advisory only: never let a lookup/persist failure change the routing
|
|
134
|
+
// decision or block the launch.
|
|
135
|
+
}
|
|
136
|
+
return result;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Bridge: alias -> descriptor -> resolveRoute (Task 4.4).
|
|
141
|
+
* Resolves a raw model string to a Descriptor — if it is a known no-slash
|
|
142
|
+
* alias (per getEffectiveAliases()), its concrete id is parsed instead, so an
|
|
143
|
+
* alias pointing at an `openrouter/...` value is treated as an explicit,
|
|
144
|
+
* force-OR literal while an alias pointing at a bare `vendor/model` is
|
|
145
|
+
* policy-routed like any other canonical id. Assembles live key/catalog state
|
|
146
|
+
* and delegates the actual decision to the pure gateway-router. Additive:
|
|
147
|
+
* not wired into any launch path yet.
|
|
148
|
+
* @param {{model:string, gatewayMode:string, source:string, allowSelection?:boolean, validateModel?:boolean}} opts
|
|
149
|
+
* @returns {Promise<object>} RouteResult (resolved | selection_required | error)
|
|
150
|
+
*/
|
|
151
|
+
async function resolveRouteForLaunch({ model, gatewayMode, source, allowSelection, validateModel }) {
|
|
152
|
+
// Lazy-required so jest.doMock('./config' | './model-descriptor' | './gateway-router', ...)
|
|
153
|
+
// can intercept them per-test, matching the pattern already used above for model-catalog.
|
|
154
|
+
const { getEffectiveAliases } = require('./config');
|
|
155
|
+
const { parseDescriptor } = require('./model-descriptor');
|
|
156
|
+
const { resolveRoute } = require('./gateway-router');
|
|
157
|
+
const aliases = getEffectiveAliases();
|
|
158
|
+
const concrete = (typeof model === 'string' && !model.includes('/') && aliases[model]) ? aliases[model] : model;
|
|
159
|
+
const descriptor = parseDescriptor(concrete, { aliases });
|
|
160
|
+
const keys = buildLaunchKeys();
|
|
161
|
+
// Skip the catalog fetch entirely under --no-validate-model: gateway-router's
|
|
162
|
+
// catalogGate short-circuits to { ok:true } as soon as validateModel === false,
|
|
163
|
+
// never consulting catalogInfo, so fetching it here would be wasted
|
|
164
|
+
// latency/network (and can hit the network on a cold cache) for no benefit.
|
|
165
|
+
// Strict === false (not just falsy) so this stays in lockstep with catalogGate's
|
|
166
|
+
// own `=== false` guard: any other value (incl. an omitted flag) still fetches,
|
|
167
|
+
// so a caller can never skip the fetch while the gate still classifies against it.
|
|
168
|
+
const catalogInfo = validateModel === false ? { models: [], lastRefreshError: null } : await getRouteCatalogInfo();
|
|
169
|
+
let result = resolveRoute({ descriptor, source, gatewayMode, allowSelection, validateModel, keys, catalogInfo });
|
|
170
|
+
if (result.kind === 'resolved') {
|
|
171
|
+
result.provenance = { ...result.provenance, resolutionVersion: ROUTE_VERSION };
|
|
172
|
+
result = maybeMigrationNotice({ result, descriptor, gatewayMode, keys });
|
|
173
|
+
} else if (result.kind === 'selection_required') {
|
|
174
|
+
result.suggestions = buildSuggestions(descriptor, keys, catalogInfo);
|
|
175
|
+
}
|
|
176
|
+
return result;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
module.exports = { buildLaunchKeys, getRouteCatalogInfo, resolveRouteForLaunch, buildSuggestions, ROUTE_VERSION };
|
|
@@ -6,71 +6,124 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
|
-
*
|
|
10
|
-
*
|
|
9
|
+
* Derive the alias used for resolution (needed downstream by the budget gate,
|
|
10
|
+
* which reports `alias || args.model` as the pricing lookup's modelInput).
|
|
11
|
+
* Per the #61 Task 4.5 design, only ever returns a no-slash token: an
|
|
12
|
+
* explicit `--model` containing '/' or a slash-bearing config default both
|
|
13
|
+
* resolve to undefined here.
|
|
11
14
|
* @param {object} args - Parsed CLI arguments
|
|
12
|
-
* @returns {
|
|
15
|
+
* @returns {string|undefined}
|
|
13
16
|
*/
|
|
14
|
-
function
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
try {
|
|
19
|
-
model = resolveModel(args.model);
|
|
20
|
-
} catch (err) {
|
|
21
|
-
console.error(err.message);
|
|
22
|
-
process.exit(1);
|
|
17
|
+
function deriveAlias(args) {
|
|
18
|
+
const raw = args.model;
|
|
19
|
+
if (raw !== undefined && raw !== null && !raw.includes('/')) {
|
|
20
|
+
return raw;
|
|
23
21
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
let alias = rawAlias;
|
|
27
|
-
if (alias === undefined) {
|
|
22
|
+
if (raw === undefined || raw === null) {
|
|
23
|
+
const { loadConfig } = require('./config');
|
|
28
24
|
const cfg = loadConfig();
|
|
29
25
|
if (cfg && cfg.default && !cfg.default.includes('/')) {
|
|
30
|
-
|
|
26
|
+
return cfg.default;
|
|
31
27
|
}
|
|
32
28
|
}
|
|
33
|
-
return
|
|
29
|
+
return undefined;
|
|
34
30
|
}
|
|
35
31
|
|
|
36
32
|
/**
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
33
|
+
* Resolve the model for a launch through the Foundation gateway router (#61
|
|
34
|
+
* Task 4.5), used by every launch path — start, continue (Task 7.3), and the
|
|
35
|
+
* MCP amicus_start handler (via the same router, Task 6.2). This replaced the
|
|
36
|
+
* legacy resolveModelFromArgs + validateFallbackModel pair, which was retired
|
|
37
|
+
* once no launch path depended on it (#61 Task 4.7).
|
|
38
|
+
*
|
|
39
|
+
* On `resolved`, returns `{ model, alias, gateway, provenance }` (and prints
|
|
40
|
+
* any advisory `notice` to stderr). On `error` or `selection_required`,
|
|
41
|
+
* renders the appropriate message to stderr and exits(1) — this never
|
|
42
|
+
* returns in that case.
|
|
40
43
|
* @param {object} args - Parsed CLI arguments
|
|
41
|
-
* @
|
|
42
|
-
* @returns {Promise<string>} Validated model string
|
|
44
|
+
* @returns {Promise<{model: string, alias: string|undefined, gateway: string, provenance: object}>}
|
|
43
45
|
*/
|
|
44
|
-
async function
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
async function resolveLaunchModel(args) {
|
|
47
|
+
const { resolveGatewayMode } = require('./config');
|
|
48
|
+
const { resolveRouteForLaunch } = require('./route-launch');
|
|
49
|
+
const { toCliMessage, toStructuredError } = require('./route-error');
|
|
50
|
+
const { resolveModelInputOrDefault } = require('./model-input-default');
|
|
48
51
|
|
|
49
|
-
const
|
|
50
|
-
const
|
|
52
|
+
const gatewayMode = resolveGatewayMode(args.gateway);
|
|
53
|
+
const validateModel = !args['no-validate-model'];
|
|
54
|
+
// Interactive alternatives picker (#61 Task 6.3): only offered on a real
|
|
55
|
+
// interactive TTY session that hasn't opted out with --no-ui. A headless/
|
|
56
|
+
// non-TTY run (CI, piped output, --no-ui) keeps allowSelection false, so a
|
|
57
|
+
// direct miss there still produces the structured error/selection_required
|
|
58
|
+
// rendered below rather than an unhandled prompt.
|
|
59
|
+
const allowSelection = !args['no-ui'] && !!process.stdin.isTTY;
|
|
60
|
+
|
|
61
|
+
// No --model given: the parser does not inject a default, so resolve the
|
|
62
|
+
// configured default here before handing off to the router. Without this, `undefined`
|
|
63
|
+
// would reach resolveRouteForLaunch -> parseDescriptor(undefined) -> an
|
|
64
|
+
// `invalid` result, breaking the common `amicus start` (no --model) case.
|
|
65
|
+
// Shared with the MCP amicus_start handler (mcp-server.js, #61 Task 6.2)
|
|
66
|
+
// via model-input-default.js, so this lookup lives in exactly one place.
|
|
67
|
+
const modelInput = resolveModelInputOrDefault(args.model);
|
|
68
|
+
if (modelInput === undefined) {
|
|
69
|
+
process.stderr.write(
|
|
70
|
+
'No model specified and no default configured. Run \'amicus setup\' to set a default model.\n'
|
|
71
|
+
);
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
51
74
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
75
|
+
const result = await resolveRouteForLaunch({
|
|
76
|
+
model: modelInput,
|
|
77
|
+
gatewayMode,
|
|
78
|
+
source: 'cli',
|
|
79
|
+
allowSelection,
|
|
80
|
+
validateModel,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
if (result.kind === 'resolved') {
|
|
84
|
+
if (result.notice) {
|
|
85
|
+
process.stderr.write(`${result.notice}\n`);
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
model: result.executableId,
|
|
89
|
+
alias: deriveAlias(args),
|
|
90
|
+
gateway: result.gateway,
|
|
91
|
+
provenance: result.provenance,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Interactive alternatives picker (#61 Task 6.3): only reachable when
|
|
96
|
+
// allowSelection was true above (real TTY, no --no-ui), so the router only
|
|
97
|
+
// ever hands back selection_required here in that same interactive case.
|
|
98
|
+
if (result.kind === 'selection_required') {
|
|
99
|
+
const { promptRouteSelection } = require('./model-validator');
|
|
55
100
|
try {
|
|
56
|
-
|
|
101
|
+
const chosen = await promptRouteSelection(result, deriveAlias(args));
|
|
102
|
+
return {
|
|
103
|
+
model: chosen.model,
|
|
104
|
+
alias: deriveAlias(args),
|
|
105
|
+
gateway: chosen.gateway,
|
|
106
|
+
provenance: result.provenance || {},
|
|
107
|
+
};
|
|
57
108
|
} catch (err) {
|
|
58
|
-
|
|
109
|
+
process.stderr.write(`${err.message || 'Model selection cancelled.'}\n`);
|
|
59
110
|
process.exit(1);
|
|
60
111
|
}
|
|
61
112
|
}
|
|
62
113
|
|
|
63
|
-
//
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
114
|
+
// 'error': render and exit. (A non-interactive run never reaches
|
|
115
|
+
// kind:'selection_required' — allowSelection is false there, so the router
|
|
116
|
+
// resolves a catalog miss straight to kind:'error' with reason
|
|
117
|
+
// 'model_not_found' instead; see gateway-router.js's catalogGate.)
|
|
118
|
+
if (args.json) {
|
|
119
|
+
process.stderr.write(`${JSON.stringify(toStructuredError(result))}\n`);
|
|
120
|
+
} else {
|
|
121
|
+
process.stderr.write(`${toCliMessage(result)}\n`);
|
|
70
122
|
}
|
|
123
|
+
process.exit(1);
|
|
71
124
|
}
|
|
72
125
|
|
|
73
126
|
module.exports = {
|
|
74
|
-
|
|
75
|
-
|
|
127
|
+
resolveLaunchModel,
|
|
128
|
+
deriveAlias,
|
|
76
129
|
};
|
package/src/utils/validators.js
CHANGED
|
@@ -19,13 +19,7 @@ const VALID_AGENT_MODES = OPENCODE_AGENTS;
|
|
|
19
19
|
/**
|
|
20
20
|
* Provider to API key mapping
|
|
21
21
|
*/
|
|
22
|
-
const PROVIDER_KEY_MAP =
|
|
23
|
-
'openrouter': { key: 'OPENROUTER_API_KEY', name: 'OpenRouter' },
|
|
24
|
-
'google': { key: 'GOOGLE_GENERATIVE_AI_API_KEY', name: 'Google Gemini' },
|
|
25
|
-
'openai': { key: 'OPENAI_API_KEY', name: 'OpenAI' },
|
|
26
|
-
'anthropic': { key: 'ANTHROPIC_API_KEY', name: 'Anthropic' },
|
|
27
|
-
'deepseek': { key: 'DEEPSEEK_API_KEY', name: 'DeepSeek' },
|
|
28
|
-
};
|
|
22
|
+
const { PROVIDER_KEY_MAP } = require('./provider-registry');
|
|
29
23
|
|
|
30
24
|
/** Task ID format: alphanumeric, hyphens, underscores, 1-64 chars */
|
|
31
25
|
const TASK_ID_PATTERN = /^[a-zA-Z0-9_-]{1,64}$/;
|
|
@@ -272,5 +266,4 @@ module.exports = {
|
|
|
272
266
|
findSessionInProjectDirs,
|
|
273
267
|
// Re-exported from input-validators.js
|
|
274
268
|
validateStartInputs: require('./input-validators').validateStartInputs,
|
|
275
|
-
findSimilar: require('./input-validators').findSimilar,
|
|
276
269
|
};
|