amicus 4.1.2 → 4.2.1
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 +48 -0
- package/README.md +21 -2
- package/bin/amicus.js +10 -0
- package/electron/ipc-setup-local.js +109 -0
- package/electron/ipc-setup.js +14 -2
- package/electron/preload-setup.js +3 -1
- package/electron/setup-ui-local-script.js +114 -0
- package/electron/setup-ui-local.js +75 -0
- package/electron/setup-ui-styles.js +15 -1
- package/electron/setup-ui.js +6 -1
- package/package.json +1 -1
- package/scripts/postinstall.js +12 -211
- package/src/cli-handlers-doctor.js +12 -0
- package/src/cli-handlers-init.js +83 -0
- package/src/cli-handlers-key-local.js +144 -0
- package/src/cli-handlers-provider.js +212 -0
- package/src/cli-handlers.js +31 -3
- package/src/cli.js +21 -0
- package/src/sidecar/setup-local.js +75 -0
- package/src/sidecar/setup.js +99 -3
- package/src/utils/api-key-store.js +17 -64
- package/src/utils/claude-register.js +267 -0
- package/src/utils/config.js +53 -1
- package/src/utils/doctor-local-providers-check.js +62 -0
- package/src/utils/doctor-summary.js +33 -0
- package/src/utils/env-loader.js +12 -0
- package/src/utils/env-raw-store.js +120 -0
- package/src/utils/gateway-router.js +65 -2
- package/src/utils/lifecycle.js +2 -1
- package/src/utils/local-probe.js +109 -0
- package/src/utils/local-providers.js +141 -0
- package/src/utils/model-catalog.js +6 -2
- package/src/utils/model-fetcher.js +11 -1
- package/src/utils/pricing.js +23 -9
- package/src/utils/provider-default-picker.js +17 -2
- package/src/utils/provider-default-prompt.js +7 -4
- package/src/utils/route-error.js +10 -3
- package/src/utils/route-launch.js +8 -65
- package/src/utils/route-suggestions.js +85 -0
package/src/utils/route-error.js
CHANGED
|
@@ -66,6 +66,9 @@ const REASON_TEXT = Object.freeze({
|
|
|
66
66
|
invalid_descriptor: 'The model identifier could not be parsed.',
|
|
67
67
|
direct_unavailable: "This model isn't available on the vendor's direct API; use OpenRouter or a different model.",
|
|
68
68
|
openrouter_unavailable: "This model isn't on OpenRouter; use --gateway direct or a different model.",
|
|
69
|
+
no_openrouter_route: "Local providers can't be routed through OpenRouter.",
|
|
70
|
+
no_local_key: 'No bearer token is configured for this local provider.',
|
|
71
|
+
local_endpoint_unreachable: "The local endpoint didn't respond.",
|
|
69
72
|
[SELECTION_REQUIRED_REASON]: 'Multiple models match your request; a specific one must be selected.',
|
|
70
73
|
});
|
|
71
74
|
|
|
@@ -80,6 +83,9 @@ const FIX_HINTS = Object.freeze({
|
|
|
80
83
|
invalid_descriptor: 'Use a vendor/model id or a configured alias.',
|
|
81
84
|
direct_unavailable: 'Drop --gateway direct (use auto or --gateway openrouter), or pick a different model.',
|
|
82
85
|
openrouter_unavailable: 'Use --gateway direct, or pick a different model.',
|
|
86
|
+
no_openrouter_route: 'Drop --gateway openrouter — local endpoints route direct.',
|
|
87
|
+
no_local_key: 'Add one with `amicus key <id> <token>`.',
|
|
88
|
+
local_endpoint_unreachable: 'Start the local server, or pass --no-validate-model to skip the reachability check.',
|
|
83
89
|
[SELECTION_REQUIRED_REASON]: 'Pick one of the suggestions below, or narrow the model id.',
|
|
84
90
|
});
|
|
85
91
|
|
|
@@ -139,7 +145,7 @@ function toCliMessage(result) {
|
|
|
139
145
|
}
|
|
140
146
|
}
|
|
141
147
|
|
|
142
|
-
const hint = FIX_HINTS[err.reason];
|
|
148
|
+
const hint = (result && result.hint) || FIX_HINTS[err.reason];
|
|
143
149
|
if (hint) { lines.push(hint); }
|
|
144
150
|
|
|
145
151
|
return lines.join('\n');
|
|
@@ -156,7 +162,8 @@ function toCliMessage(result) {
|
|
|
156
162
|
*/
|
|
157
163
|
function toErrorDocFields(result) {
|
|
158
164
|
const { ERROR_CODES } = require('./error-doc');
|
|
159
|
-
|
|
165
|
+
// v4.2: a missing local bearer is key-shaped, not model-shaped (D12).
|
|
166
|
+
const KEY_REASONS = ['no_openrouter_key', 'no_direct_key', 'no_key_for_vendor', 'no_local_key'];
|
|
160
167
|
const err = toStructuredError(result);
|
|
161
168
|
const sentence = REASON_TEXT[err.reason] || `Model routing error (${err.reason}).`;
|
|
162
169
|
let message = err.requested ? `${sentence} (requested "${err.requested}")` : sentence;
|
|
@@ -166,7 +173,7 @@ function toErrorDocFields(result) {
|
|
|
166
173
|
return {
|
|
167
174
|
code: KEY_REASONS.includes(err.reason) ? ERROR_CODES.MISSING_KEY : ERROR_CODES.BAD_MODEL,
|
|
168
175
|
message,
|
|
169
|
-
hint: FIX_HINTS[err.reason] || null,
|
|
176
|
+
hint: (result && result.hint) || FIX_HINTS[err.reason] || null,
|
|
170
177
|
};
|
|
171
178
|
}
|
|
172
179
|
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
const { readApiKeys } = require('./api-key-store');
|
|
12
12
|
const { readAuthJsonKeys } = require('./auth-json');
|
|
13
13
|
const { KNOWN_PROVIDERS } = require('./provider-registry');
|
|
14
|
+
const { buildSuggestions, applySuggestions } = require('./route-suggestions');
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* Per-provider key presence across BOTH sources: env/.env (readApiKeys) and
|
|
@@ -50,69 +51,6 @@ async function getRouteCatalogInfo() {
|
|
|
50
51
|
/** Module version stamped onto `resolved` results' provenance (carry-forward). */
|
|
51
52
|
const ROUTE_VERSION = 1;
|
|
52
53
|
|
|
53
|
-
/**
|
|
54
|
-
* Build up to ~6 labeled alternatives for a `selection_required` RouteResult
|
|
55
|
-
* (#61 Task 6.3, spec Decision 10). Pure: reads only the already-parsed
|
|
56
|
-
* descriptor plus the live keys/catalogInfo/gatewayIds the caller already
|
|
57
|
-
* assembled.
|
|
58
|
-
*
|
|
59
|
-
* Two categories, in order:
|
|
60
|
-
* 1. The SAME model via OpenRouter — only when an OpenRouter key is present
|
|
61
|
-
* AND the OR-namespaced id is actually present in the catalog (never
|
|
62
|
-
* suggest an id we can't confirm exists). For divergent vendors (e.g.
|
|
63
|
-
* Anthropic) `descriptor.model` may be the DASH-form direct id, so a
|
|
64
|
-
* reconstructed `openrouter/<vendor>/<model>` would never match the
|
|
65
|
-
* catalog's dot-form OR id -- when the caller's `gatewayIds.openrouter`
|
|
66
|
-
* is available (the catalog-correct form), it is used instead of
|
|
67
|
-
* reconstructing. Falls back to reconstruction when `gatewayIds` is
|
|
68
|
-
* absent (non-alias / full-id / non-divergent requests), so behavior
|
|
69
|
-
* there is unchanged.
|
|
70
|
-
* 2. Up to 5 OTHER models in the same direct vendor namespace (ids starting
|
|
71
|
-
* `<vendor>/`, excluding the requested id itself and excluding any
|
|
72
|
-
* `openrouter/`-prefixed rows, which share the `<vendor>/` prefix check
|
|
73
|
-
* only when vendor === 'openrouter' and are filtered out defensively).
|
|
74
|
-
*
|
|
75
|
-
* @param {{vendor?: string, model?: string}} descriptor parsed Descriptor for
|
|
76
|
-
* the request that produced the selection_required (canonical or
|
|
77
|
-
* openrouter-literal — both carry vendor/model)
|
|
78
|
-
* @param {Object<string,boolean>} keys per-provider key-presence map (buildLaunchKeys() shape)
|
|
79
|
-
* @param {{models: Array<{id:string}>}} catalogInfo
|
|
80
|
-
* @param {{direct?: string, openrouter?: string}} [gatewayIds] the same
|
|
81
|
-
* per-gateway id map resolveRouteForLaunch threads through resolveRoute
|
|
82
|
-
* (Task 3's bridge for divergent curated aliases); absent for non-alias /
|
|
83
|
-
* full-id / non-divergent requests
|
|
84
|
-
* @returns {Array<{model:string, gateway:string, note:string}>}
|
|
85
|
-
*/
|
|
86
|
-
function buildSuggestions(descriptor, keys, catalogInfo, gatewayIds) {
|
|
87
|
-
const suggestions = [];
|
|
88
|
-
const vendor = descriptor && descriptor.vendor;
|
|
89
|
-
const model = descriptor && descriptor.model;
|
|
90
|
-
if (!vendor || !model) { return suggestions; }
|
|
91
|
-
|
|
92
|
-
const models = (catalogInfo && Array.isArray(catalogInfo.models)) ? catalogInfo.models : [];
|
|
93
|
-
const requestedDirectId = `${vendor}/${model}`;
|
|
94
|
-
|
|
95
|
-
if (keys && keys.openrouter) {
|
|
96
|
-
const orId = (gatewayIds && gatewayIds.openrouter) || `openrouter/${vendor}/${model}`;
|
|
97
|
-
if (models.some(m => m && m.id === orId)) {
|
|
98
|
-
suggestions.push({ model: orId, gateway: 'openrouter', note: 'same model via OpenRouter' });
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
const nsPrefix = `${vendor}/`;
|
|
103
|
-
const sameVendor = models.filter(m =>
|
|
104
|
-
m && typeof m.id === 'string' &&
|
|
105
|
-
m.id.startsWith(nsPrefix) &&
|
|
106
|
-
!m.id.startsWith('openrouter/') &&
|
|
107
|
-
m.id !== requestedDirectId
|
|
108
|
-
).slice(0, 5);
|
|
109
|
-
for (const m of sameVendor) {
|
|
110
|
-
suggestions.push({ model: m.id, gateway: 'direct', note: `${vendor} model` });
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
return suggestions.slice(0, 6);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
54
|
/**
|
|
117
55
|
* One-time per-vendor notice when auto-routing migrates a both-key holder off
|
|
118
56
|
* OpenRouter onto direct (#61 Task 5.1 — visible-migration guarantee: never
|
|
@@ -286,12 +224,17 @@ async function resolveRouteForLaunch({ model, gatewayMode, source, allowSelectio
|
|
|
286
224
|
}
|
|
287
225
|
}
|
|
288
226
|
const descriptor = parseDescriptor(concrete, { aliases });
|
|
289
|
-
|
|
227
|
+
// v4.2: local-provider inputs (assembled in local-providers.js — 300-line gate).
|
|
228
|
+
const { getLocalProviders, resolveLocalRouteInputs } = require('./local-providers');
|
|
229
|
+
const { localProviders, localLive } =
|
|
230
|
+
await resolveLocalRouteInputs(descriptor, { validateModel, providers: getLocalProviders() });
|
|
231
|
+
let result = resolveRoute({ descriptor, source, gatewayMode, allowSelection, validateModel,
|
|
232
|
+
keys, catalogInfo, gatewayIds, localProviders, localLive });
|
|
290
233
|
if (result.kind === 'resolved') {
|
|
291
234
|
result.provenance = { ...result.provenance, resolutionVersion: ROUTE_VERSION };
|
|
292
235
|
result = maybeMigrationNotice({ result, descriptor, gatewayMode, keys });
|
|
293
236
|
} else if (result.kind === 'selection_required') {
|
|
294
|
-
result
|
|
237
|
+
applySuggestions(result, { descriptor, keys, catalogInfo, gatewayIds, localProviders });
|
|
295
238
|
}
|
|
296
239
|
return result;
|
|
297
240
|
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Suggestion-building for `selection_required` RouteResults, split out of
|
|
5
|
+
* route-launch.js (B2/D3 — the 300-line gate). Pure: no I/O, no requires.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Build up to ~6 labeled alternatives for a `selection_required` RouteResult
|
|
10
|
+
* (#61 Task 6.3, spec Decision 10). Pure: reads only the already-parsed
|
|
11
|
+
* descriptor plus the live keys/catalogInfo/gatewayIds the caller already
|
|
12
|
+
* assembled.
|
|
13
|
+
*
|
|
14
|
+
* Two categories, in order:
|
|
15
|
+
* 1. The SAME model via OpenRouter — only when an OpenRouter key is present
|
|
16
|
+
* AND the OR-namespaced id is actually present in the catalog (never
|
|
17
|
+
* suggest an id we can't confirm exists). For divergent vendors (e.g.
|
|
18
|
+
* Anthropic) `descriptor.model` may be the DASH-form direct id, so a
|
|
19
|
+
* reconstructed `openrouter/<vendor>/<model>` would never match the
|
|
20
|
+
* catalog's dot-form OR id -- when the caller's `gatewayIds.openrouter`
|
|
21
|
+
* is available (the catalog-correct form), it is used instead of
|
|
22
|
+
* reconstructing. Falls back to reconstruction when `gatewayIds` is
|
|
23
|
+
* absent (non-alias / full-id / non-divergent requests), so behavior
|
|
24
|
+
* there is unchanged.
|
|
25
|
+
* 2. Up to 5 OTHER models in the same direct vendor namespace (ids starting
|
|
26
|
+
* `<vendor>/`, excluding the requested id itself and excluding any
|
|
27
|
+
* `openrouter/`-prefixed rows, which share the `<vendor>/` prefix check
|
|
28
|
+
* only when vendor === 'openrouter' and are filtered out defensively).
|
|
29
|
+
*
|
|
30
|
+
* @param {{vendor?: string, model?: string}} descriptor parsed Descriptor for
|
|
31
|
+
* the request that produced the selection_required (canonical or
|
|
32
|
+
* openrouter-literal — both carry vendor/model)
|
|
33
|
+
* @param {Object<string,boolean>} keys per-provider key-presence map (buildLaunchKeys() shape)
|
|
34
|
+
* @param {{models: Array<{id:string}>}} catalogInfo
|
|
35
|
+
* @param {{direct?: string, openrouter?: string}} [gatewayIds] the same
|
|
36
|
+
* per-gateway id map resolveRouteForLaunch threads through resolveRoute
|
|
37
|
+
* (Task 3's bridge for divergent curated aliases); absent for non-alias /
|
|
38
|
+
* full-id / non-divergent requests
|
|
39
|
+
* @returns {Array<{model:string, gateway:string, note:string}>}
|
|
40
|
+
*/
|
|
41
|
+
function buildSuggestions(descriptor, keys, catalogInfo, gatewayIds) {
|
|
42
|
+
const suggestions = [];
|
|
43
|
+
const vendor = descriptor && descriptor.vendor;
|
|
44
|
+
const model = descriptor && descriptor.model;
|
|
45
|
+
if (!vendor || !model) { return suggestions; }
|
|
46
|
+
|
|
47
|
+
const models = (catalogInfo && Array.isArray(catalogInfo.models)) ? catalogInfo.models : [];
|
|
48
|
+
const requestedDirectId = `${vendor}/${model}`;
|
|
49
|
+
|
|
50
|
+
if (keys && keys.openrouter) {
|
|
51
|
+
const orId = (gatewayIds && gatewayIds.openrouter) || `openrouter/${vendor}/${model}`;
|
|
52
|
+
if (models.some(m => m && m.id === orId)) {
|
|
53
|
+
suggestions.push({ model: orId, gateway: 'openrouter', note: 'same model via OpenRouter' });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const nsPrefix = `${vendor}/`;
|
|
58
|
+
const sameVendor = models.filter(m =>
|
|
59
|
+
m && typeof m.id === 'string' &&
|
|
60
|
+
m.id.startsWith(nsPrefix) &&
|
|
61
|
+
!m.id.startsWith('openrouter/') &&
|
|
62
|
+
m.id !== requestedDirectId
|
|
63
|
+
).slice(0, 5);
|
|
64
|
+
for (const m of sameVendor) {
|
|
65
|
+
suggestions.push({ model: m.id, gateway: 'direct', note: `${vendor} model` });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return suggestions.slice(0, 6);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* v4.2 (D10/M24): apply catalog-derived suggestions to a `selection_required`
|
|
73
|
+
* result — EXCEPT for a local vendor, whose suggestions the router already
|
|
74
|
+
* built from the live probe (spec §4.2 point 4: "suggestions = the live list,
|
|
75
|
+
* capped at 6"). buildSuggestions reads only `catalogInfo.models` (the 24 h
|
|
76
|
+
* cache, never `req.localLive`) and hardcodes `gateway:'direct'`, so letting it
|
|
77
|
+
* overwrite a local result ships stale-or-empty rows under the wrong label.
|
|
78
|
+
* @param {Object} result the `selection_required` RouteResult, mutated in place
|
|
79
|
+
*/
|
|
80
|
+
function applySuggestions(result, { descriptor, keys, catalogInfo, gatewayIds, localProviders }) {
|
|
81
|
+
if (localProviders && descriptor && Object.prototype.hasOwnProperty.call(localProviders, descriptor.vendor)) { return; }
|
|
82
|
+
result.suggestions = buildSuggestions(descriptor, keys, catalogInfo, gatewayIds);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
module.exports = { buildSuggestions, applySuggestions };
|