dsh-connect-trae 4.0.1 → 4.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +109 -0
- package/README.en.md +1 -0
- package/README.md +1 -0
- package/lib/client.js +218 -3
- package/lib/index.d.ts +214 -2
- package/lib/index.js +477 -39
- package/package.json +3 -2
package/lib/index.js
CHANGED
|
@@ -235,12 +235,15 @@ function mergeTraeModelSources(remote, wire) {
|
|
|
235
235
|
for (const model of remote) {
|
|
236
236
|
const wireModel = wireById.get(displayKey(model.id)) ?? wireByName.get(displayKey(model.name));
|
|
237
237
|
const creditMultiplier = wireModel?.creditMultiplier ?? model.creditMultiplier;
|
|
238
|
+
const wireFunction = wireModel?.function ?? model.function;
|
|
239
|
+
const activityDiscount = wireModel?.activityDiscount ?? model.activityDiscount;
|
|
238
240
|
result.push({
|
|
239
241
|
id: model.id,
|
|
240
242
|
name: model.name,
|
|
241
243
|
...model.contextWindow === void 0 ? {} : { contextWindow: model.contextWindow },
|
|
242
244
|
...model.maxContextWindow === void 0 ? {} : { maxContextWindow: model.maxContextWindow },
|
|
243
245
|
...creditMultiplier === void 0 ? {} : { creditMultiplier },
|
|
246
|
+
...activityDiscount === void 0 ? {} : { activityDiscount },
|
|
244
247
|
input: ["text"],
|
|
245
248
|
...wireModel?.maxTokens === void 0 ? {} : { maxTokens: wireModel.maxTokens },
|
|
246
249
|
...model.multimodal === void 0 ? {} : { multimodal: model.multimodal },
|
|
@@ -250,12 +253,45 @@ function mergeTraeModelSources(remote, wire) {
|
|
|
250
253
|
reasoningEfforts: Object.fromEntries(model.reasoning.supported.map((effort) => [effort, effort === "low" ? "light" : effort === "xhigh" ? "extra_high" : "high"]))
|
|
251
254
|
},
|
|
252
255
|
...wireModel !== void 0 && wireModel.id !== "" && wireModel.id !== model.id ? { wireConfigName: wireModel.id } : {},
|
|
253
|
-
...
|
|
256
|
+
...wireFunction === void 0 ? {} : { wireFunction }
|
|
254
257
|
});
|
|
255
258
|
}
|
|
256
259
|
return result;
|
|
257
260
|
}
|
|
258
261
|
/**
|
|
262
|
+
* Demote a NON-chat `wireFunction` to "no function", keeping the row.
|
|
263
|
+
*
|
|
264
|
+
* A row pinned to an IDE/Remote-agent roster — `solo_agent`, `builder_v3` — is
|
|
265
|
+
* not callable through `llm_utils_chat` by THAT function: asking answers
|
|
266
|
+
* 4011/4001 (measured 2026-10-01, issue #19). Dropping the row is not the fix,
|
|
267
|
+
* though, and was a regression: a model can carry a non-chat function for a
|
|
268
|
+
* reason that says nothing about its chat availability. The directory lists
|
|
269
|
+
* `gpt-5.5`, `gpt-5.6-*`, `gpt-6-astra`, `glm-5.2`, `kimi-k2.7-code`,
|
|
270
|
+
* `Dola-Seed-2.0-Code` and `deepseek-v4-flash-0731` under `solo_agent` ALONE
|
|
271
|
+
* (docs/ISSUE19_DIAGNOSIS.md), yet the same models answer normally under
|
|
272
|
+
* `solo_work_remote` / `solo_work_lite` / `solo_agent_remote` — they are
|
|
273
|
+
* exactly the models issue #19's fix was opened to expose. Dropping them
|
|
274
|
+
* re-hid the whole set on the restricted networks this fallback targets, where
|
|
275
|
+
* the wire roster is empty and the directory group is the only attribution.
|
|
276
|
+
*
|
|
277
|
+
* Keeping the row with the function CLEARED preserves both facts: the model
|
|
278
|
+
* stays visible (issue #19), and the bridge sends the region's DEFAULT chat
|
|
279
|
+
* function instead of one that is guaranteed to refuse. That is the pre-change
|
|
280
|
+
* behaviour for these rows — a genuinely unreachable model still fails with a
|
|
281
|
+
* readable upstream message rather than a silent absence.
|
|
282
|
+
*
|
|
283
|
+
* `chatFunctions` must be the region's own `TRAE_DIRECTORY_FUNCTIONS` list —
|
|
284
|
+
* the functions `llm_utils_chat` accepts. Callers should log the demoted rows:
|
|
285
|
+
* "which function did it lose, and why" deserves an answer.
|
|
286
|
+
*/
|
|
287
|
+
function withChatCallableFunctions(rows, chatFunctions) {
|
|
288
|
+
return rows.map((model) => {
|
|
289
|
+
if (model.wireFunction === void 0 || chatFunctions.includes(model.wireFunction)) return model;
|
|
290
|
+
const { wireFunction: _demoted, ...withoutFunction } = model;
|
|
291
|
+
return withoutFunction;
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
259
295
|
* Apply the saved local budget. Trae advertises two windows per model (dev and
|
|
260
296
|
* Max), so the budget may only switch a model to its own advertised Max value —
|
|
261
297
|
* never to a fabricated number. Everything else keeps the dev window.
|
|
@@ -276,7 +312,9 @@ function discoveredCatalog(models) {
|
|
|
276
312
|
...model.maxContextWindow === void 0 ? {} : { maxContextWindow: model.maxContextWindow },
|
|
277
313
|
input: ["text"],
|
|
278
314
|
...model.creditMultiplier === void 0 ? {} : { creditMultiplier: model.creditMultiplier },
|
|
315
|
+
...model.activityDiscount === void 0 ? {} : { activityDiscount: model.activityDiscount },
|
|
279
316
|
...model.multimodal === void 0 ? {} : { multimodal: model.multimodal },
|
|
317
|
+
...model.function === void 0 ? {} : { wireFunction: model.function },
|
|
280
318
|
reasoningSupported: model.reasoningSupported,
|
|
281
319
|
...model.reasoning === void 0 ? {} : {
|
|
282
320
|
reasoning: model.reasoning,
|
|
@@ -3703,6 +3741,97 @@ function traeEndpoint(baseUrl, path) {
|
|
|
3703
3741
|
return `${baseUrl.replace(/\/$/, "")}/${path.replace(/^\//, "")}`;
|
|
3704
3742
|
}
|
|
3705
3743
|
//#endregion
|
|
3744
|
+
//#region src/promo.ts
|
|
3745
|
+
function record$1(value) {
|
|
3746
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
3747
|
+
}
|
|
3748
|
+
function finite(value) {
|
|
3749
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
3750
|
+
}
|
|
3751
|
+
function percent(value) {
|
|
3752
|
+
const parsed = finite(value);
|
|
3753
|
+
return parsed !== void 0 && parsed > 0 && parsed < 100 ? parsed : void 0;
|
|
3754
|
+
}
|
|
3755
|
+
function ratePair(raw) {
|
|
3756
|
+
const entry = record$1(raw);
|
|
3757
|
+
if (entry === void 0) return {};
|
|
3758
|
+
const beforeRate = finite(entry["before_consumption_rate"]);
|
|
3759
|
+
const rate = finite(entry["consumption_rate"]) ?? finite(entry["after_consumption_rate"]);
|
|
3760
|
+
const discountPercent = percent(entry["discount"]);
|
|
3761
|
+
return {
|
|
3762
|
+
...beforeRate === void 0 || beforeRate <= 0 ? {} : { beforeRate },
|
|
3763
|
+
...rate === void 0 || rate <= 0 ? {} : { rate },
|
|
3764
|
+
...discountPercent === void 0 ? {} : { discountPercent }
|
|
3765
|
+
};
|
|
3766
|
+
}
|
|
3767
|
+
function windows(raw) {
|
|
3768
|
+
const list = Array.isArray(raw) ? raw : [];
|
|
3769
|
+
const parsed = [];
|
|
3770
|
+
for (const item of list) {
|
|
3771
|
+
const entry = record$1(item);
|
|
3772
|
+
if (entry === void 0) continue;
|
|
3773
|
+
const weekdays = Array.isArray(entry["weekdays"]) ? entry["weekdays"].filter((day) => Number.isInteger(day) && day >= 1 && day <= 7) : [];
|
|
3774
|
+
const startMinute = finite(entry["start_minute"]);
|
|
3775
|
+
const endMinute = finite(entry["end_minute"]);
|
|
3776
|
+
if (weekdays.length === 0 || startMinute === void 0 || endMinute === void 0) continue;
|
|
3777
|
+
if (startMinute < 0 || startMinute > 1439 || endMinute < 1 || endMinute > 1440) continue;
|
|
3778
|
+
if (startMinute >= endMinute) continue;
|
|
3779
|
+
parsed.push({
|
|
3780
|
+
weekdays,
|
|
3781
|
+
startMinute,
|
|
3782
|
+
endMinute
|
|
3783
|
+
});
|
|
3784
|
+
}
|
|
3785
|
+
return parsed;
|
|
3786
|
+
}
|
|
3787
|
+
/**
|
|
3788
|
+
* Parse one `activity_discount` document.
|
|
3789
|
+
*
|
|
3790
|
+
* `value` may be the already-parsed object (both call sites hand us the parent
|
|
3791
|
+
* object) or the raw JSON string; anything without `enable: true` reads as "no
|
|
3792
|
+
* promotion" rather than as a rule we might later misjudge.
|
|
3793
|
+
*/
|
|
3794
|
+
function parseTraeActivityDiscount(value) {
|
|
3795
|
+
let source = value;
|
|
3796
|
+
if (typeof source === "string") {
|
|
3797
|
+
if (source === "") return void 0;
|
|
3798
|
+
try {
|
|
3799
|
+
source = JSON.parse(source);
|
|
3800
|
+
} catch {
|
|
3801
|
+
return;
|
|
3802
|
+
}
|
|
3803
|
+
}
|
|
3804
|
+
const raw = record$1(source);
|
|
3805
|
+
if (raw === void 0 || raw["enable"] !== true) return void 0;
|
|
3806
|
+
const data = record$1(raw["data"]);
|
|
3807
|
+
if (data === void 0) return void 0;
|
|
3808
|
+
const subKey = typeof raw["subKey"] === "string" && raw["subKey"] !== "" ? raw["subKey"] : void 0;
|
|
3809
|
+
const currentRaw = record$1(data["current"]);
|
|
3810
|
+
const currentType = typeof currentRaw?.["discount_type"] === "string" ? currentRaw["discount_type"] : void 0;
|
|
3811
|
+
const current = currentRaw === void 0 ? void 0 : {
|
|
3812
|
+
...ratePair(currentRaw),
|
|
3813
|
+
...currentType === void 0 ? {} : { discountType: currentType }
|
|
3814
|
+
};
|
|
3815
|
+
const memberRaw = data["member"];
|
|
3816
|
+
const member = record$1(memberRaw) === void 0 ? void 0 : ratePair(memberRaw);
|
|
3817
|
+
const subsidyRaw = data["subsidy"];
|
|
3818
|
+
const subsidy = record$1(subsidyRaw) === void 0 ? void 0 : ratePair(subsidyRaw);
|
|
3819
|
+
const offPeakRaw = record$1(data["off_peak"]);
|
|
3820
|
+
const offPeak = offPeakRaw === void 0 ? void 0 : {
|
|
3821
|
+
...ratePair(offPeakRaw),
|
|
3822
|
+
...typeof offPeakRaw["time_zone"] === "string" && offPeakRaw["time_zone"] !== "" ? { timeZone: offPeakRaw["time_zone"] } : {},
|
|
3823
|
+
windows: windows(offPeakRaw["time_windows"])
|
|
3824
|
+
};
|
|
3825
|
+
const parsed = {
|
|
3826
|
+
...subKey === void 0 ? {} : { subKey },
|
|
3827
|
+
...current === void 0 ? {} : { current },
|
|
3828
|
+
...member === void 0 ? {} : { member },
|
|
3829
|
+
...offPeak === void 0 ? {} : { offPeak },
|
|
3830
|
+
...subsidy === void 0 ? {} : { subsidy }
|
|
3831
|
+
};
|
|
3832
|
+
return parsed.current === void 0 && parsed.member === void 0 && parsed.offPeak === void 0 && parsed.subsidy === void 0 ? void 0 : parsed;
|
|
3833
|
+
}
|
|
3834
|
+
//#endregion
|
|
3706
3835
|
//#region src/reasoning.ts
|
|
3707
3836
|
const TRAE_REASONING_EFFORTS = [
|
|
3708
3837
|
"minimal",
|
|
@@ -3870,6 +3999,29 @@ function prepareSoloBody(source, defaultModel = "glm-5.2", functionName) {
|
|
|
3870
3999
|
return JSON.stringify(body);
|
|
3871
4000
|
}
|
|
3872
4001
|
/**
|
|
4002
|
+
* Parse this row's `display_contact_config` document.
|
|
4003
|
+
*
|
|
4004
|
+
* It is a *string* holding a second JSON document, and three different readers
|
|
4005
|
+
* (the credit multiplier, the promotion rule, and whatever upstream adds next)
|
|
4006
|
+
* need it — so it is decoded once here rather than three times.
|
|
4007
|
+
*/
|
|
4008
|
+
function wireContactConfig(config) {
|
|
4009
|
+
const raw = config["display_contact_config"];
|
|
4010
|
+
if (typeof raw !== "string" || raw === "") return void 0;
|
|
4011
|
+
let parsed;
|
|
4012
|
+
try {
|
|
4013
|
+
parsed = JSON.parse(raw);
|
|
4014
|
+
} catch {
|
|
4015
|
+
return;
|
|
4016
|
+
}
|
|
4017
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return void 0;
|
|
4018
|
+
return parsed;
|
|
4019
|
+
}
|
|
4020
|
+
/** The promotion rule from `display_contact_config`, when the row carries one. */
|
|
4021
|
+
function wireActivityDiscount(config) {
|
|
4022
|
+
return parseTraeActivityDiscount(wireContactConfig(config)?.["activity_discount"]);
|
|
4023
|
+
}
|
|
4024
|
+
/**
|
|
3873
4025
|
* Read the effective credit multiplier from a `get_detail_param` row.
|
|
3874
4026
|
*
|
|
3875
4027
|
* The rate the Trae IDE renders lives in `display_contact_config` — a *string*
|
|
@@ -3885,15 +4037,8 @@ function prepareSoloBody(source, defaultModel = "glm-5.2", functionName) {
|
|
|
3885
4037
|
* field keep their bare name — parsed-but-absent, never fabricated.
|
|
3886
4038
|
*/
|
|
3887
4039
|
function wireCreditMultiplier(config) {
|
|
3888
|
-
const
|
|
3889
|
-
if (
|
|
3890
|
-
let parsed;
|
|
3891
|
-
try {
|
|
3892
|
-
parsed = JSON.parse(raw);
|
|
3893
|
-
} catch {
|
|
3894
|
-
return;
|
|
3895
|
-
}
|
|
3896
|
-
if (typeof parsed !== "object" || parsed === null) return void 0;
|
|
4040
|
+
const parsed = wireContactConfig(config);
|
|
4041
|
+
if (parsed === void 0) return void 0;
|
|
3897
4042
|
const consumption = parsed["consumption_rate"];
|
|
3898
4043
|
if (typeof consumption !== "object" || consumption === null) return void 0;
|
|
3899
4044
|
const entry = consumption;
|
|
@@ -4001,6 +4146,7 @@ var TraeSoloUpstreamClient = class {
|
|
|
4001
4146
|
...detail
|
|
4002
4147
|
});
|
|
4003
4148
|
const creditMultiplier = wireCreditMultiplier(config);
|
|
4149
|
+
const activityDiscount = wireActivityDiscount(config);
|
|
4004
4150
|
if (byId.has(id)) continue;
|
|
4005
4151
|
byId.set(id, {
|
|
4006
4152
|
id,
|
|
@@ -4009,6 +4155,7 @@ var TraeSoloUpstreamClient = class {
|
|
|
4009
4155
|
...maxTokens === void 0 ? {} : { maxTokens },
|
|
4010
4156
|
...reasoning === void 0 ? {} : { reasoning },
|
|
4011
4157
|
...creditMultiplier === void 0 ? {} : { creditMultiplier },
|
|
4158
|
+
...activityDiscount === void 0 ? {} : { activityDiscount },
|
|
4012
4159
|
function: directoryFunction
|
|
4013
4160
|
});
|
|
4014
4161
|
}
|
|
@@ -4702,8 +4849,9 @@ var TraeSoloBridge = class {
|
|
|
4702
4849
|
if (typeof input["model"] === "string" && input["model"] !== "") model = input["model"];
|
|
4703
4850
|
const entryKey = model.trim().toLowerCase();
|
|
4704
4851
|
const entry = this.catalog?.current().find((item) => item.id === model || item.id.trim().toLowerCase() === entryKey);
|
|
4705
|
-
const
|
|
4706
|
-
|
|
4852
|
+
const catalogConfigName = entry?.wireConfigName;
|
|
4853
|
+
const fromCatalog = entry === void 0 || catalogConfigName === void 0 && entry.wireFunction === void 0 ? void 0 : {
|
|
4854
|
+
configName: catalogConfigName ?? entry.id,
|
|
4707
4855
|
...entry.wireFunction === void 0 ? {} : { function: entry.wireFunction }
|
|
4708
4856
|
};
|
|
4709
4857
|
const fromResolver = this.wireResolver?.(model) ?? this.wireResolver?.(entry?.name ?? "");
|
|
@@ -4718,11 +4866,28 @@ var TraeSoloBridge = class {
|
|
|
4718
4866
|
* directory, so borrowing its function is safe exactly when both sources
|
|
4719
4867
|
* agree on the wire config_name; a disagreement means the resolver found
|
|
4720
4868
|
* a different row and its function would be wrong.
|
|
4869
|
+
*
|
|
4870
|
+
* The MIRROR case is equally load-bearing and is the reason the row's own
|
|
4871
|
+
* id is not accepted as its wire spelling: a row that carries only the
|
|
4872
|
+
* FUNCTION (`wireConfigName` absent) has no config_name of its own, and
|
|
4873
|
+
* its display id is the wrong-cased spelling of the wire name in exactly
|
|
4874
|
+
* the cases issue #38 is about. The resolver's config_name is then the
|
|
4875
|
+
* only correct one, so it must win for the NAME while the row keeps
|
|
4876
|
+
* winning for the FUNCTION.
|
|
4721
4877
|
*/
|
|
4722
4878
|
const sameWireConfig = fromCatalog !== void 0 && fromResolver !== void 0 && fromCatalog.configName.toLowerCase() === fromResolver.configName.toLowerCase();
|
|
4879
|
+
/**
|
|
4880
|
+
* Whether the catalog row knows its own wire `config_name` (as opposed to
|
|
4881
|
+
* only carrying the display id plus a function). Only such a row may
|
|
4882
|
+
* decide the name on its own.
|
|
4883
|
+
*/
|
|
4884
|
+
const catalogKnowsConfigName = catalogConfigName !== void 0;
|
|
4723
4885
|
const target = fromCatalog !== void 0 && fromCatalog.function === void 0 && sameWireConfig && fromResolver !== void 0 ? {
|
|
4724
4886
|
configName: fromCatalog.configName,
|
|
4725
4887
|
...fromResolver.function === void 0 ? {} : { function: fromResolver.function }
|
|
4888
|
+
} : fromCatalog !== void 0 && !catalogKnowsConfigName && fromResolver !== void 0 ? {
|
|
4889
|
+
configName: fromResolver.configName,
|
|
4890
|
+
...fromCatalog.function === void 0 ? {} : { function: fromCatalog.function }
|
|
4726
4891
|
} : fromCatalog ?? fromResolver;
|
|
4727
4892
|
/**
|
|
4728
4893
|
* Last line of defence: the discovered row's own id.
|
|
@@ -4786,8 +4951,15 @@ const EFFORT_MAP = {
|
|
|
4786
4951
|
high: "high",
|
|
4787
4952
|
extra_high: "xhigh"
|
|
4788
4953
|
};
|
|
4789
|
-
/**
|
|
4790
|
-
|
|
4954
|
+
/**
|
|
4955
|
+
* Parse only capabilities explicitly advertised by Trae's remote model API.
|
|
4956
|
+
*
|
|
4957
|
+
* @param groupFunction The `data.list[].function` of the group this raw row
|
|
4958
|
+
* was read from; attached verbatim so downstream can replay the model's own
|
|
4959
|
+
* function instead of the `llm_utils_chat` default (see
|
|
4960
|
+
* {@link TraeDiscoveredModel.function}).
|
|
4961
|
+
*/
|
|
4962
|
+
function parseTraeRemoteModel(value, groupFunction) {
|
|
4791
4963
|
const raw = record(value);
|
|
4792
4964
|
if (raw === void 0 || typeof raw.name !== "string" || raw.name === "") return void 0;
|
|
4793
4965
|
const context = record(raw.context_window_tokens);
|
|
@@ -4798,6 +4970,7 @@ function parseTraeRemoteModel(value) {
|
|
|
4798
4970
|
const consumptionData = record(consumption?.["data"]);
|
|
4799
4971
|
const creditMultiplier = consumption?.["enable"] === true ? finitePositive(consumptionData?.["rate"]) : void 0;
|
|
4800
4972
|
const reasoningSupported = record(features?.["reasoning"])?.["enable"] === true;
|
|
4973
|
+
const activityDiscount = parseTraeActivityDiscount(features?.["activity_discount"]);
|
|
4801
4974
|
const reasoningConfig = record(raw.reasoning_effort_config);
|
|
4802
4975
|
const supported = (Array.isArray(reasoningConfig?.["options"]) ? reasoningConfig["options"] : []).flatMap((option) => {
|
|
4803
4976
|
if (typeof option !== "string") return [];
|
|
@@ -4814,11 +4987,13 @@ function parseTraeRemoteModel(value) {
|
|
|
4814
4987
|
...dev === void 0 ? {} : { contextWindow: dev },
|
|
4815
4988
|
...max === void 0 ? {} : { maxContextWindow: max },
|
|
4816
4989
|
...creditMultiplier === void 0 ? {} : { creditMultiplier },
|
|
4990
|
+
...activityDiscount === void 0 ? {} : { activityDiscount },
|
|
4817
4991
|
reasoningSupported,
|
|
4818
4992
|
...supported.length === 0 ? {} : { reasoning: {
|
|
4819
4993
|
supported,
|
|
4820
4994
|
...defaultEffort === void 0 ? {} : { defaultEffort }
|
|
4821
|
-
} }
|
|
4995
|
+
} },
|
|
4996
|
+
...groupFunction === void 0 || groupFunction === "" ? {} : { function: groupFunction }
|
|
4822
4997
|
};
|
|
4823
4998
|
}
|
|
4824
4999
|
//#endregion
|
|
@@ -4885,8 +5060,24 @@ const TRAE_REMOTE_DIRECTORY_FUNCTIONS = {
|
|
|
4885
5060
|
]
|
|
4886
5061
|
};
|
|
4887
5062
|
/**
|
|
4888
|
-
*
|
|
4889
|
-
*
|
|
5063
|
+
* Backup bases for the model directory, in try order after the official base.
|
|
5064
|
+
*
|
|
5065
|
+
* In restricted (proxy-sandboxed) networks the official catalog host
|
|
5066
|
+
* `solo.trae.cn` is behind a CONNECT rejection while the region's chat gateway
|
|
5067
|
+
* stays reachable — and that gateway serves the SAME `/api/remote/v1/models`
|
|
5068
|
+
* endpoint with an identical response shape (verified against
|
|
5069
|
+
* `https://trae-api-cn.mchost.guru/api/remote/v1` 2026-10-20; `chat` already
|
|
5070
|
+
* carries `/api/remote/v1`'s parent in {@link REGION_GATEWAYS}). The backup is
|
|
5071
|
+
* therefore derived from the region's chat base, mirrored on the remote path;
|
|
5072
|
+
* `ai` shares a single host for chat and remote, so it yields no candidate and
|
|
5073
|
+
* keeps its single-host behavior. An explicit `baseUrl` disables the chain.
|
|
5074
|
+
*/
|
|
5075
|
+
function traeSoloRemoteFallbackBases(region, officialBase) {
|
|
5076
|
+
const backupBase = `${REGION_GATEWAYS[region].chat}/api/remote/v1`;
|
|
5077
|
+
return backupBase === officialBase ? [] : [backupBase];
|
|
5078
|
+
}
|
|
5079
|
+
/**
|
|
5080
|
+
* Of two rows for the SAME model id, the one the catalog should keep.
|
|
4890
5081
|
*
|
|
4891
5082
|
* The gateway groups its directory by function and reports a model's capacity
|
|
4892
5083
|
* per group, with the group order varying between calls. Keeping the strongest
|
|
@@ -4894,6 +5085,9 @@ const TRAE_REMOTE_DIRECTORY_FUNCTIONS = {
|
|
|
4894
5085
|
* LEXICOGRAPHIC and must be applied all the way down, or ordering leaks back in
|
|
4895
5086
|
* at the next field:
|
|
4896
5087
|
*
|
|
5088
|
+
* 0. the kept row must also carry the promotional rule, which the groups
|
|
5089
|
+
* disagree about — the rule is copied onto the capacity winner, and of two
|
|
5090
|
+
* rules the one that can say more wins (see the comment in the body);
|
|
4897
5091
|
* 1. a row with a Max tier beats one without (`maxContextWindow` is only set
|
|
4898
5092
|
* when the group reported `max_mode: true` with a positive max);
|
|
4899
5093
|
* 2. two Max rows: the larger max wins;
|
|
@@ -4911,6 +5105,52 @@ const TRAE_REMOTE_DIRECTORY_FUNCTIONS = {
|
|
|
4911
5105
|
*/
|
|
4912
5106
|
function preferStrongerRow(candidate, incumbent) {
|
|
4913
5107
|
if (incumbent === void 0) return candidate;
|
|
5108
|
+
const winner = strongerCapacityRow(candidate, incumbent);
|
|
5109
|
+
const rule = richerRule(candidate.activityDiscount, incumbent.activityDiscount);
|
|
5110
|
+
if (rule === void 0 || rule === winner.activityDiscount) return winner;
|
|
5111
|
+
return {
|
|
5112
|
+
...winner,
|
|
5113
|
+
activityDiscount: rule
|
|
5114
|
+
};
|
|
5115
|
+
}
|
|
5116
|
+
/**
|
|
5117
|
+
* Step 0's tie-break: of two rules for the same model id, the one that can say
|
|
5118
|
+
* more about this moment. Every parsed rule already passed `enable: true` (see
|
|
5119
|
+
* parseTraeActivityDiscount), so the ranking is: an upstream "right now"
|
|
5120
|
+
* discount first, then the rule covering more clock. Two rules that rank EQUAL
|
|
5121
|
+
* are compared by their canonical content, so the answer depends on the rule
|
|
5122
|
+
* documents and never on which group the gateway answered first (issue #23).
|
|
5123
|
+
*/
|
|
5124
|
+
function richerRule(candidate, incumbent) {
|
|
5125
|
+
if (candidate === void 0) return incumbent;
|
|
5126
|
+
if (incumbent === void 0) return candidate;
|
|
5127
|
+
const score = (rule) => {
|
|
5128
|
+
return (rule.current !== void 0 && (rule.current.discountType ?? "none") !== "none" ? 1 : 0) * 1e3 + (rule.offPeak?.windows.length ?? 0);
|
|
5129
|
+
};
|
|
5130
|
+
const candidateScore = score(candidate);
|
|
5131
|
+
const incumbentScore = score(incumbent);
|
|
5132
|
+
if (candidateScore !== incumbentScore) return candidateScore > incumbentScore ? candidate : incumbent;
|
|
5133
|
+
return canonicalRule(candidate) < canonicalRule(incumbent) ? candidate : incumbent;
|
|
5134
|
+
}
|
|
5135
|
+
/**
|
|
5136
|
+
* Content-only identity for two rules that scored the same.
|
|
5137
|
+
*
|
|
5138
|
+
* The score cannot distinguish, say, two `current: none` rules with two windows
|
|
5139
|
+
* each in different hours — and keeping "whichever arrived first" would let the
|
|
5140
|
+
* gateway's group order decide whether tonight's badge appears. Key order is
|
|
5141
|
+
* normalised (the objects are ours, but a caller may hand us a literal), so
|
|
5142
|
+
* equal documents compare equal and the incumbent is kept unchanged.
|
|
5143
|
+
*/
|
|
5144
|
+
function canonicalRule(rule) {
|
|
5145
|
+
const canonical = (value) => {
|
|
5146
|
+
if (Array.isArray(value)) return value.map((item) => canonical(item));
|
|
5147
|
+
if (typeof value !== "object" || value === null) return value;
|
|
5148
|
+
return Object.fromEntries(Object.entries(value).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0).map(([key, item]) => [key, canonical(item)]));
|
|
5149
|
+
};
|
|
5150
|
+
return JSON.stringify(canonical(rule));
|
|
5151
|
+
}
|
|
5152
|
+
/** Steps 1–4 of the documented order: the row advertising the stronger capacity. */
|
|
5153
|
+
function strongerCapacityRow(candidate, incumbent) {
|
|
4914
5154
|
const candidateMax = candidate.maxContextWindow;
|
|
4915
5155
|
const incumbentMax = incumbent.maxContextWindow;
|
|
4916
5156
|
if (candidateMax !== void 0 && incumbentMax === void 0) return candidate;
|
|
@@ -4942,10 +5182,39 @@ function remoteDressing(region) {
|
|
|
4942
5182
|
* This deliberately has no chat/session method: the Remote session protocol
|
|
4943
5183
|
* only exposes a final answer and cannot preserve DSH's structured tool loop.
|
|
4944
5184
|
*/
|
|
4945
|
-
var TraeSoloRemoteCatalogClient = class {
|
|
5185
|
+
var TraeSoloRemoteCatalogClient = class TraeSoloRemoteCatalogClient {
|
|
4946
5186
|
options;
|
|
4947
5187
|
fetchImpl;
|
|
4948
5188
|
baseUrl;
|
|
5189
|
+
/**
|
|
5190
|
+
* The last base that answered the directory with a NON-EMPTY catalog in this
|
|
5191
|
+
* process.
|
|
5192
|
+
*
|
|
5193
|
+
* A proxy-blocked network pays the failing official attempt on EVERY refresh
|
|
5194
|
+
* otherwise, so a backup that has served the catalog is tried first on later
|
|
5195
|
+
* refreshes. Putting the official base "second" is NOT enough to re-probe it:
|
|
5196
|
+
* the loop returns on the first success, so a backup that keeps answering
|
|
5197
|
+
* means the official base is never asked again. The official host is
|
|
5198
|
+
* therefore re-probed on a COOLDOWN instead (see {@link OFFICIAL_RETRY_MS}):
|
|
5199
|
+
* while the cooldown runs the backup is used and the official host is
|
|
5200
|
+
* skipped; when it expires the official host is tried FIRST again, so a
|
|
5201
|
+
* healed network takes over within one cooldown window rather than never.
|
|
5202
|
+
*/
|
|
5203
|
+
lastSuccessfulBase;
|
|
5204
|
+
/**
|
|
5205
|
+
* When the official base may be probed again after a transport/5xx failure,
|
|
5206
|
+
* as epoch milliseconds (0 = probe it on the next refresh).
|
|
5207
|
+
*/
|
|
5208
|
+
officialRetryAtMs = 0;
|
|
5209
|
+
/**
|
|
5210
|
+
* How long a failing official host is skipped before it is re-probed.
|
|
5211
|
+
*
|
|
5212
|
+
* Long enough that a blocked network stops paying the failing attempt on
|
|
5213
|
+
* every refresh (the point of the fallback), short enough that a healed
|
|
5214
|
+
* network takes over without a restart. Each probe costs one connect timeout
|
|
5215
|
+
* (30 s) in the worst case, so 5 minutes bounds that to ~1 attempt per 5 min.
|
|
5216
|
+
*/
|
|
5217
|
+
static OFFICIAL_RETRY_MS = 3e5;
|
|
4949
5218
|
constructor(options) {
|
|
4950
5219
|
this.options = options;
|
|
4951
5220
|
this.fetchImpl = options.fetchImpl ?? fetch;
|
|
@@ -4966,24 +5235,183 @@ var TraeSoloRemoteCatalogClient = class {
|
|
|
4966
5235
|
}
|
|
4967
5236
|
async fetchModels(signal) {
|
|
4968
5237
|
const region = regionOfCredential(await this.options.credential());
|
|
4969
|
-
const
|
|
5238
|
+
const officialBase = this.baseUrl ?? REGION_GATEWAYS[region].remote;
|
|
4970
5239
|
const headers = await this.headers(region);
|
|
4971
5240
|
const functions = TRAE_REMOTE_DIRECTORY_FUNCTIONS[region].join(",");
|
|
4972
|
-
|
|
4973
|
-
|
|
4974
|
-
|
|
4975
|
-
|
|
4976
|
-
|
|
4977
|
-
|
|
5241
|
+
let lastError;
|
|
5242
|
+
let emptyError;
|
|
5243
|
+
for (const base of this.candidateBases(region, officialBase)) {
|
|
5244
|
+
let response;
|
|
5245
|
+
try {
|
|
5246
|
+
response = await this.fetchImpl(`${base}/models?functions=${functions}`, {
|
|
5247
|
+
headers,
|
|
5248
|
+
signal: signal ?? AbortSignal.timeout(3e4)
|
|
5249
|
+
});
|
|
5250
|
+
} catch (error) {
|
|
5251
|
+
if (signal?.aborted) throw error;
|
|
5252
|
+
if (base === officialBase) this.officialRetryAtMs = Date.now() + TraeSoloRemoteCatalogClient.OFFICIAL_RETRY_MS;
|
|
5253
|
+
lastError = error;
|
|
5254
|
+
continue;
|
|
5255
|
+
}
|
|
5256
|
+
if (!response.ok) {
|
|
5257
|
+
if (response.status < 500) throw new Error(`SOLO remote models returned HTTP ${response.status}`);
|
|
5258
|
+
if (base === officialBase) this.officialRetryAtMs = Date.now() + TraeSoloRemoteCatalogClient.OFFICIAL_RETRY_MS;
|
|
5259
|
+
lastError = /* @__PURE__ */ new Error(`SOLO remote models returned HTTP ${response.status}`);
|
|
5260
|
+
continue;
|
|
5261
|
+
}
|
|
5262
|
+
const json = await response.json();
|
|
5263
|
+
const models = this.collectModels(region, json.data?.list ?? []);
|
|
5264
|
+
if (models.length === 0) {
|
|
5265
|
+
emptyError = /* @__PURE__ */ new Error("SOLO remote models response contained no models");
|
|
5266
|
+
lastError = emptyError;
|
|
5267
|
+
continue;
|
|
5268
|
+
}
|
|
5269
|
+
if (base !== officialBase) this.options.log?.(this.lastSuccessfulBase === base ? `model directory served by the remembered fallback host ${base}; the official host (${officialBase}) is re-probed every ${TraeSoloRemoteCatalogClient.OFFICIAL_RETRY_MS / 6e4} minutes` : `model directory served by fallback host ${base}; the official host (${officialBase}) failed this refresh`, { officialBase });
|
|
5270
|
+
else this.officialRetryAtMs = 0;
|
|
5271
|
+
this.lastSuccessfulBase = base;
|
|
5272
|
+
return models;
|
|
5273
|
+
}
|
|
5274
|
+
if (emptyError !== void 0) throw emptyError;
|
|
5275
|
+
throw lastError instanceof Error ? lastError : new Error(String(lastError));
|
|
5276
|
+
}
|
|
5277
|
+
/**
|
|
5278
|
+
* Bases to try for one refresh, in order.
|
|
5279
|
+
*
|
|
5280
|
+
* While the official host is in its post-failure cooldown the backup hosts go
|
|
5281
|
+
* first and the official base is kept LAST as a final chance (so a still-down
|
|
5282
|
+
* backup does not fail the whole refresh while the official host happens to
|
|
5283
|
+
* be reachable). Once the cooldown expires the official base is first again:
|
|
5284
|
+
* that is what lets a healed network take over — the loop returns on the
|
|
5285
|
+
* first success, so simply placing the official base second never re-probes
|
|
5286
|
+
* it while a backup keeps answering.
|
|
5287
|
+
*/
|
|
5288
|
+
candidateBases(region, officialBase) {
|
|
5289
|
+
if (this.baseUrl !== void 0) return [officialBase];
|
|
5290
|
+
const fallbacks = traeSoloRemoteFallbackBases(region, officialBase);
|
|
5291
|
+
if (this.officialRetryAtMs <= Date.now()) return [officialBase, ...fallbacks];
|
|
5292
|
+
const remembered = this.lastSuccessfulBase;
|
|
5293
|
+
return [
|
|
5294
|
+
...remembered === void 0 ? [] : [remembered],
|
|
5295
|
+
...fallbacks.filter((base) => base !== remembered),
|
|
5296
|
+
officialBase
|
|
5297
|
+
];
|
|
5298
|
+
}
|
|
5299
|
+
/**
|
|
5300
|
+
* Union every returned group into one deduplicated list, attributing each
|
|
5301
|
+
* model to the CHAT function the directory listed it under.
|
|
5302
|
+
*
|
|
5303
|
+
* Each group is a roster the gateway is willing to serve; a model listed by
|
|
5304
|
+
* any of them is a model the IDE can offer, and the wire join downstream (see
|
|
5305
|
+
* mergeTraeModelSources) is what decides whether it is actually callable.
|
|
5306
|
+
* Restricting this to `solo_agent_remote` dropped the nine models issue #19
|
|
5307
|
+
* was opened about.
|
|
5308
|
+
*
|
|
5309
|
+
* FUNCTION ATTRIBUTION (2026-10-20): a group is the authoritative statement
|
|
5310
|
+
* of which SOLO function a model belongs to — the accepted `config_name` is
|
|
5311
|
+
* isolated per function (glm-5.3 answers `solo_work_remote` and 4001s under
|
|
5312
|
+
* `solo_work_lite`). The wire join prefers its own `get_detail_param`
|
|
5313
|
+
* function and falls back to this one when the roster says nothing, which
|
|
5314
|
+
* restricted networks often do (measured: an empty `config_info_list` for
|
|
5315
|
+
* every chat function but `chat_v3`).
|
|
5316
|
+
*
|
|
5317
|
+
* ONLY CHAT-CAPABLE GROUPS ARE ATTRIBUTED. `solo_agent` and `builder_v3` are
|
|
5318
|
+
* IDE/Remote-agent rosters: asking `llm_utils_chat` with either answers
|
|
5319
|
+
* 4011/4001 (measured 2026-10-01 on a live credential, issue #19). A model
|
|
5320
|
+
* that appears ONLY there is still not uncallable, though — the SAME models
|
|
5321
|
+
* answered normally under `solo_work_remote` / `solo_work_lite` /
|
|
5322
|
+
* `solo_agent_remote` (measured the same day, see `TRAE_DIRECTORY_FUNCTIONS`
|
|
5323
|
+
* in solo.ts), and the default function is what has always reached them:
|
|
5324
|
+
* they are exactly the nine models issue #19's fix exposes (`gpt-5.5`,
|
|
5325
|
+
* `gpt-5.6-*`, `gpt-6-astra`, `glm-5.2`, `kimi-k2.7-code`,
|
|
5326
|
+
* `Dola-Seed-2.0-Code`, `deepseek-v4-flash-0731` — docs/ISSUE19_DIAGNOSIS.md).
|
|
5327
|
+
* Stamping `solo_agent` onto such a row makes every call 4011; DROPPING it
|
|
5328
|
+
* undoes issue #19 for exactly the restricted-network users this fallback
|
|
5329
|
+
* exists for. It therefore keeps NO function, the bridge falls back to the
|
|
5330
|
+
* default chat function exactly as before this change, and a refusal (if the
|
|
5331
|
+
* model really is unreachable) comes back as a readable upstream message
|
|
5332
|
+
* instead of a silent absence.
|
|
5333
|
+
*
|
|
5334
|
+
* CHAT GROUPS ARE WALKED IN `TRAE_DIRECTORY_FUNCTIONS` ORDER, not in the
|
|
5335
|
+
* request's `functions=` order. That list is the measured precedence of which
|
|
5336
|
+
* function serves a shared config, and it is the order the wire roster itself
|
|
5337
|
+
* collects in (`solo.ts`) — walking a different order would attribute the
|
|
5338
|
+
* same model differently depending on whether the roster answered, and the
|
|
5339
|
+
* difference is not cosmetic: `solo_agent_remote` sits after the two
|
|
5340
|
+
* `solo_work_*` functions because it is behind the Remote plan gate, so
|
|
5341
|
+
* preferring it can turn a free-tier model into a `1005 requires a paid
|
|
5342
|
+
* plan` refusal.
|
|
5343
|
+
*
|
|
5344
|
+
* A model appears in SEVERAL groups and the groups disagree about its
|
|
5345
|
+
* capacity, so dedupe keeps the MOST CAPABLE row rather than the first one
|
|
5346
|
+
* (issue #23). "First wins" assumed the group order was stable; measured
|
|
5347
|
+
* 2026-10-05 on a live CN credential it is not — five calls returned five
|
|
5348
|
+
* different orders — so the published window became a coin flip:
|
|
5349
|
+
* `Doubao-Seed-2.1-Pro` came back as `116000`/no-Max (`builder_v3`), or
|
|
5350
|
+
* `256000`/no-Max (`solo_work_*`), or `256000`/`1000000` (`chat_v3`,
|
|
5351
|
+
* `solo_agent*`) depending on which group answered first. The Max tier is
|
|
5352
|
+
* the one users enable in the IDE, and `applyContextBudgets` can only raise
|
|
5353
|
+
* a window to `maxContextWindow` — so losing that field silently capped
|
|
5354
|
+
* every configured 1M model at 256K or less, and DSH compressed far earlier
|
|
5355
|
+
* than the chosen budget implied.
|
|
5356
|
+
*
|
|
5357
|
+
* Ordering rule: a row advertising a Max tier beats one that does not, a
|
|
5358
|
+
* larger Max wins over a smaller one, and with no Max on either side the
|
|
5359
|
+
* wider dev window wins. Every row for one id carries the same
|
|
5360
|
+
* `multimodal` / `reasoning` / credit facts (verified the same day), so
|
|
5361
|
+
* choosing on capacity loses no capability. The result no longer depends on
|
|
5362
|
+
* the gateway's group order — and the FUNCTION does not either: the winning
|
|
5363
|
+
* row's capacity may come from a later group, but the separately-tracked
|
|
5364
|
+
* attribution is re-asserted on it.
|
|
5365
|
+
*/
|
|
5366
|
+
collectModels(region, groups) {
|
|
4978
5367
|
const byId = /* @__PURE__ */ new Map();
|
|
4979
|
-
|
|
4980
|
-
|
|
4981
|
-
|
|
4982
|
-
|
|
5368
|
+
const attributedFunction = /* @__PURE__ */ new Map();
|
|
5369
|
+
/**
|
|
5370
|
+
* The function a row may be stamped with: only a CHAT function. A non-chat
|
|
5371
|
+
* roster is not "the function this model belongs to" in any sense the chat
|
|
5372
|
+
* route can use (see the method note above), so it is not recorded at all.
|
|
5373
|
+
*/
|
|
5374
|
+
const chatFunctions = new Set(TRAE_DIRECTORY_FUNCTIONS[region]);
|
|
5375
|
+
const attribution = (groupFunction) => groupFunction !== void 0 && chatFunctions.has(groupFunction) ? groupFunction : void 0;
|
|
5376
|
+
const addGroup = (groupFunction, raws) => {
|
|
5377
|
+
const fn = attribution(groupFunction);
|
|
5378
|
+
for (const raw of raws) {
|
|
5379
|
+
const model = parseTraeRemoteModel(raw, fn);
|
|
5380
|
+
if (model === void 0) continue;
|
|
5381
|
+
if (fn !== void 0 && !attributedFunction.has(model.id)) attributedFunction.set(model.id, fn);
|
|
5382
|
+
byId.set(model.id, preferStrongerRow(model, byId.get(model.id)));
|
|
5383
|
+
}
|
|
5384
|
+
};
|
|
5385
|
+
/**
|
|
5386
|
+
* Every group the gateway sent, bucketed BY FUNCTION NAME and kept as a
|
|
5387
|
+
* LIST.
|
|
5388
|
+
*
|
|
5389
|
+
* A name→group `Map` would keep only the LAST group of a name and silently
|
|
5390
|
+
* drop the models of every earlier one: nothing documents that the gateway
|
|
5391
|
+
* sends a function at most once, and the pre-change code unioned every
|
|
5392
|
+
* element of `data.list`, so a repeated name lost real models. Bucketing
|
|
5393
|
+
* keeps that union while the walk below still imposes a deterministic
|
|
5394
|
+
* order.
|
|
5395
|
+
*/
|
|
5396
|
+
const byFunction = /* @__PURE__ */ new Map();
|
|
5397
|
+
for (const group of groups) {
|
|
5398
|
+
const key = group.function ?? "";
|
|
5399
|
+
const bucket = byFunction.get(key);
|
|
5400
|
+
if (bucket === void 0) byFunction.set(key, [group]);
|
|
5401
|
+
else bucket.push(group);
|
|
4983
5402
|
}
|
|
4984
|
-
const
|
|
4985
|
-
|
|
4986
|
-
|
|
5403
|
+
for (const name of TRAE_DIRECTORY_FUNCTIONS[region]) for (const group of byFunction.get(name) ?? []) addGroup(name, group.models ?? []);
|
|
5404
|
+
for (const [name, bucket] of byFunction) {
|
|
5405
|
+
if (chatFunctions.has(name)) continue;
|
|
5406
|
+
for (const group of bucket) addGroup(name, group.models ?? []);
|
|
5407
|
+
}
|
|
5408
|
+
return [...byId.values()].map((model) => {
|
|
5409
|
+
const fn = attributedFunction.get(model.id);
|
|
5410
|
+
return fn === void 0 || fn === model.function ? model : {
|
|
5411
|
+
...model,
|
|
5412
|
+
function: fn
|
|
5413
|
+
};
|
|
5414
|
+
});
|
|
4987
5415
|
}
|
|
4988
5416
|
};
|
|
4989
5417
|
//#endregion
|
|
@@ -8490,7 +8918,8 @@ const modelConfig = z.object({
|
|
|
8490
8918
|
creditMultiplier: z.number(),
|
|
8491
8919
|
wireFunction: z.string(),
|
|
8492
8920
|
wireConfigName: z.string(),
|
|
8493
|
-
multimodal: z.boolean()
|
|
8921
|
+
multimodal: z.boolean(),
|
|
8922
|
+
activityDiscount: z.any()
|
|
8494
8923
|
});
|
|
8495
8924
|
/**
|
|
8496
8925
|
* One panel's saved state. Panels are keyed `cn | enterprise | ai` — see
|
|
@@ -8756,7 +9185,10 @@ function apply(ctx, config) {
|
|
|
8756
9185
|
...panel === "enterprise" ? { route: "enterprise" } : {},
|
|
8757
9186
|
log: (message, detail) => ctx.logger.warn(message, detail)
|
|
8758
9187
|
});
|
|
8759
|
-
const remoteCatalog = new TraeSoloRemoteCatalogClient({
|
|
9188
|
+
const remoteCatalog = new TraeSoloRemoteCatalogClient({
|
|
9189
|
+
credential: () => store.resolve(),
|
|
9190
|
+
log: (message, detail) => ctx.logger.warn(message, detail)
|
|
9191
|
+
});
|
|
8760
9192
|
const wireResolver = (displayId) => wire.byId.get(displayId) ?? wire.byName.get(displayId.trim().toLowerCase());
|
|
8761
9193
|
/**
|
|
8762
9194
|
* One client + bridge per provider id.
|
|
@@ -8909,18 +9341,24 @@ function apply(ctx, config) {
|
|
|
8909
9341
|
},
|
|
8910
9342
|
discoverModels: async (signal) => {
|
|
8911
9343
|
const merged = mergeTraeModelSources(await withDirectoryRetry(() => remoteCatalog.fetchModels(signal)), await solo.fetchModels(signal));
|
|
8912
|
-
|
|
9344
|
+
const callable = withChatCallableFunctions(merged, TRAE_DIRECTORY_FUNCTIONS[region]);
|
|
9345
|
+
const chatFunctions = TRAE_DIRECTORY_FUNCTIONS[region];
|
|
8913
9346
|
for (const model of merged) {
|
|
9347
|
+
if (model.wireFunction === void 0 || chatFunctions.includes(model.wireFunction)) continue;
|
|
9348
|
+
ctx.logger.warn(`model directory row is pinned to a non-chat function and will be called through the default chat function instead: ${model.id} (pinned to function ${model.wireFunction})`);
|
|
9349
|
+
}
|
|
9350
|
+
wire.callableKeys.clear();
|
|
9351
|
+
for (const model of callable) {
|
|
8914
9352
|
wire.callableKeys.add(model.id.trim().toLowerCase());
|
|
8915
9353
|
wire.callableKeys.add(model.name.trim().toLowerCase());
|
|
8916
9354
|
}
|
|
8917
|
-
wire.resolved =
|
|
8918
|
-
const wireTargets = wireTargetsOf(
|
|
9355
|
+
wire.resolved = callable.length > 0;
|
|
9356
|
+
const wireTargets = wireTargetsOf(callable);
|
|
8919
9357
|
wire.byId = wireTargets.byId;
|
|
8920
9358
|
wire.byName = wireTargets.byName;
|
|
8921
9359
|
latestDiscovery.length = 0;
|
|
8922
|
-
latestDiscovery.push(...
|
|
8923
|
-
return
|
|
9360
|
+
latestDiscovery.push(...callable);
|
|
9361
|
+
return callable;
|
|
8924
9362
|
},
|
|
8925
9363
|
/**
|
|
8926
9364
|
* One-click availability test. The ids come from the card, so each one is
|