vibeostheog 0.19.8 → 0.20.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 +9 -0
- package/README.md +10 -10
- package/package.json +1 -1
- package/src/index.js +254 -58
- package/src/lib/hooks/footer.js +27 -23
- package/src/lib/hooks/tool-execute.js +13 -2
- package/src/lib/pricing.js +140 -3
- package/src/lib/trinity-rebuild.js +74 -19
- package/src/lib/trinity-tool.js +5 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
## 0.20.0
|
|
2
|
+
- fix: resolve live OpenCode model and refresh README launch copy
|
|
3
|
+
Merge pull request #61 from DrunkkToys/codex/release-candidate-blackbox-footer
|
|
4
|
+
Merge pull request #59 from DrunkkToys/codex/fix-thinking-directive-precedence
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
## 0.19.9
|
|
8
|
+
- fix: show live run model in footer
|
|
9
|
+
|
|
1
10
|
## 0.19.8
|
|
2
11
|
- fix: shorten live footer alerts
|
|
3
12
|
- fix: add /g flag and \b word boundaries to ERROR_SIGNAL_WORDS regex
|
package/README.md
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
# vibeOS for OpenCode
|
|
2
2
|
|
|
3
|
-
> **Alpha
|
|
4
|
-
vibeOS is the cost-aware routing layer for OpenCode Desktop. It keeps high-tier models focused on orchestration, pushes implementation work to cheaper tiers, and makes the savings visible in real time through the live footer and dashboard.
|
|
3
|
+
> **Alpha Omega Launch** - This release is the first major public launch of vibeOS. See [CHANGELOG.md](CHANGELOG.md) for release notes.
|
|
5
4
|
|
|
5
|
+
vibeOS is the cost-aware control plane for OpenCode Desktop. It helps individuals and teams keep expensive models focused on strategy, move implementation work to cheaper tiers, and make the resulting savings visible in real time through the live footer and dashboard.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
For teams, vibeOS adds practical guardrails: delegation enforcement, flow and TDD controls, pattern learning, stress-aware routing, blackbox decision tracking, reporting, and remote API protection for the core algorithms.
|
|
8
8
|
|
|
9
|
-
##
|
|
9
|
+
## What We Offer
|
|
10
10
|
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
11
|
+
- Model routing that matches the job to the right provider and tier
|
|
12
|
+
- Live savings visibility in chat, the footer, and the web dashboard
|
|
13
|
+
- Separate tracking for delegation savings and cache savings
|
|
14
|
+
- Runtime controls for flow, TDD, model locking, and blackbox mode
|
|
15
|
+
- A local fallback path if the remote API is unavailable
|
|
16
16
|
|
|
17
17
|
## Install
|
|
18
18
|
|
|
@@ -99,7 +99,7 @@ Additional reporting commands:
|
|
|
99
99
|
|
|
100
100
|
The footer shows:
|
|
101
101
|
|
|
102
|
-
- the active model
|
|
102
|
+
- the active provider/model in use for the current run
|
|
103
103
|
- cumulative delegation savings
|
|
104
104
|
- cache savings
|
|
105
105
|
- stress level
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vibeostheog",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.0",
|
|
4
4
|
"description": "Cost-aware delegation enforcer for OpenCode. Tracks model usage, routes Task subagents to cheaper tiers, surfaces cumulative savings in chat. Includes research audit, reporting framework, project memory, progressive scratchpad decadence, and trinity CLI for brain/medium/cheap slot switching.",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"release": "node scripts/release.mjs",
|
package/src/index.js
CHANGED
|
@@ -563,7 +563,7 @@ function json(res, statusCode, data) {
|
|
|
563
563
|
res.end(JSON.stringify(data));
|
|
564
564
|
}
|
|
565
565
|
function parseBody(req) {
|
|
566
|
-
return new Promise((
|
|
566
|
+
return new Promise((resolve2, reject) => {
|
|
567
567
|
let raw = "";
|
|
568
568
|
req.on("data", (chunk) => {
|
|
569
569
|
raw += String(chunk || "");
|
|
@@ -573,11 +573,11 @@ function parseBody(req) {
|
|
|
573
573
|
});
|
|
574
574
|
req.on("end", () => {
|
|
575
575
|
if (!raw.trim()) {
|
|
576
|
-
|
|
576
|
+
resolve2({});
|
|
577
577
|
return;
|
|
578
578
|
}
|
|
579
579
|
try {
|
|
580
|
-
|
|
580
|
+
resolve2(JSON.parse(raw));
|
|
581
581
|
} catch {
|
|
582
582
|
reject(new Error("invalid request"));
|
|
583
583
|
}
|
|
@@ -823,14 +823,14 @@ function createMcpServer(deps) {
|
|
|
823
823
|
return server2;
|
|
824
824
|
if (startPromise)
|
|
825
825
|
return startPromise;
|
|
826
|
-
startPromise = new Promise((
|
|
826
|
+
startPromise = new Promise((resolve2, reject) => {
|
|
827
827
|
const srv = http.createServer((req, res) => {
|
|
828
828
|
void handler(req, res);
|
|
829
829
|
});
|
|
830
830
|
srv.once("error", reject);
|
|
831
831
|
srv.listen(port, () => {
|
|
832
832
|
server2 = srv;
|
|
833
|
-
|
|
833
|
+
resolve2(srv);
|
|
834
834
|
});
|
|
835
835
|
});
|
|
836
836
|
try {
|
|
@@ -844,8 +844,8 @@ function createMcpServer(deps) {
|
|
|
844
844
|
return;
|
|
845
845
|
if (closePromise)
|
|
846
846
|
return closePromise;
|
|
847
|
-
closePromise = new Promise((
|
|
848
|
-
server2?.close((err) => err ? reject(err) :
|
|
847
|
+
closePromise = new Promise((resolve2, reject) => {
|
|
848
|
+
server2?.close((err) => err ? reject(err) : resolve2());
|
|
849
849
|
});
|
|
850
850
|
try {
|
|
851
851
|
await closePromise;
|
|
@@ -1441,7 +1441,7 @@ async function remoteCall(method, args, fallbackFn) {
|
|
|
1441
1441
|
|
|
1442
1442
|
// src/lib/pricing.js
|
|
1443
1443
|
import { readFileSync as readFileSync5, writeFileSync as writeFileSync5, appendFileSync as appendFileSync4, existsSync as existsSync6, mkdirSync as mkdirSync5, statSync as statSync5, copyFileSync as copyFileSync3, renameSync as renameSync4, openSync as openSync2, closeSync as closeSync2, rmSync as rmSync3, readdirSync as readdirSync2 } from "node:fs";
|
|
1444
|
-
import { join as join5, dirname as dirname6, basename as basename4 } from "node:path";
|
|
1444
|
+
import { join as join5, dirname as dirname6, basename as basename4, resolve } from "node:path";
|
|
1445
1445
|
import { homedir as homedir4, tmpdir as tmpdir3 } from "node:os";
|
|
1446
1446
|
import { createHash as createHash2 } from "node:crypto";
|
|
1447
1447
|
|
|
@@ -3684,6 +3684,9 @@ function getVibeOSHome4() {
|
|
|
3684
3684
|
function getOpenCodeHome() {
|
|
3685
3685
|
return process.env.VIBEOS_OPENCODE_HOME || join5(process.env.HOME || homedir4(), ".config", "opencode");
|
|
3686
3686
|
}
|
|
3687
|
+
function getOpenCodeDesktopHome() {
|
|
3688
|
+
return process.env.VIBEOS_OPENCODE_DESKTOP_HOME || join5(process.env.HOME || homedir4(), "Library", "Application Support", "ai.opencode.desktop");
|
|
3689
|
+
}
|
|
3687
3690
|
var TIERS_FILE2 = join5(getVibeOSHome4(), "model-tiers.json");
|
|
3688
3691
|
function _handleStateCorruption3(path) {
|
|
3689
3692
|
const backupDir = join5(getVibeOSHome4(), ".backups");
|
|
@@ -3802,7 +3805,7 @@ var MODEL_USD_PER_TURN = {
|
|
|
3802
3805
|
"haiku": 22e-4,
|
|
3803
3806
|
// ── DeepSeek (OC platform + OpenRouter) ──────────────────
|
|
3804
3807
|
"deepseek/deepseek-v4-pro": 57e-5,
|
|
3805
|
-
"deepseek/deepseek-v4-flash":
|
|
3808
|
+
"deepseek/deepseek-v4-flash": 182e-6,
|
|
3806
3809
|
"deepseek/deepseek-chat": 182e-6,
|
|
3807
3810
|
"deepseek-chat": 182e-6,
|
|
3808
3811
|
"deepseek/deepseek-v3": 182e-6,
|
|
@@ -4114,8 +4117,88 @@ function loadSelection2() {
|
|
|
4114
4117
|
var DFLT_SEL2 = { enabled: true, active_slot: null, thinking_level: "off", flow_enabled: false, tdd_enforce: false, tdd_strict: false, tdd_quality: true, flow_enforce: false, delegation_enforce: true };
|
|
4115
4118
|
function readConfig(dir) {
|
|
4116
4119
|
try {
|
|
4117
|
-
const
|
|
4118
|
-
|
|
4120
|
+
const configs = [];
|
|
4121
|
+
const workspaceModel = readWorkspaceSessionModel(dir);
|
|
4122
|
+
if (workspaceModel)
|
|
4123
|
+
return workspaceModel;
|
|
4124
|
+
const projectCfg = readOpenCodeConfigObject(dir);
|
|
4125
|
+
if (projectCfg && typeof projectCfg === "object")
|
|
4126
|
+
configs.push(projectCfg);
|
|
4127
|
+
const homeDir = getOpenCodeHome();
|
|
4128
|
+
if (dir !== homeDir) {
|
|
4129
|
+
const homeCfg = readOpenCodeConfigObject(homeDir);
|
|
4130
|
+
if (homeCfg && typeof homeCfg === "object")
|
|
4131
|
+
configs.push(homeCfg);
|
|
4132
|
+
}
|
|
4133
|
+
const selectedCfg = configs[0] || {};
|
|
4134
|
+
const selectedModel = selectedCfg?.agent?.build?.model || selectedCfg?.model || "";
|
|
4135
|
+
return resolveConfiguredModelId(selectedModel, configs);
|
|
4136
|
+
} catch {
|
|
4137
|
+
return "";
|
|
4138
|
+
}
|
|
4139
|
+
}
|
|
4140
|
+
function readWorkspaceSessionModel(directory3 = "") {
|
|
4141
|
+
const sid = readLatestOpenCodeSessionId(directory3);
|
|
4142
|
+
if (!sid)
|
|
4143
|
+
return "";
|
|
4144
|
+
const roots = [getOpenCodeDesktopHome(), getOpenCodeHome()];
|
|
4145
|
+
for (const root of roots) {
|
|
4146
|
+
try {
|
|
4147
|
+
if (!existsSync6(root) || !statSync5(root).isDirectory())
|
|
4148
|
+
continue;
|
|
4149
|
+
const files = readdirSync2(root).filter((name) => /^opencode\.workspace\..*\.dat$/i.test(name)).map((name) => join5(root, name)).sort((a, b) => statSync5(b).mtimeMs - statSync5(a).mtimeMs);
|
|
4150
|
+
for (const file of files) {
|
|
4151
|
+
try {
|
|
4152
|
+
const raw = readFileSync5(file, "utf-8");
|
|
4153
|
+
if (!raw.includes(sid) || !raw.includes("workspace:model-selection"))
|
|
4154
|
+
continue;
|
|
4155
|
+
const match = raw.match(/"workspace:model-selection"\s*:\s*"((?:\\.|[^"\\])*)"/s);
|
|
4156
|
+
if (!match)
|
|
4157
|
+
continue;
|
|
4158
|
+
const decoded = JSON.parse(`"${match[1]}"`);
|
|
4159
|
+
const parsed = safeJsonParse3(decoded);
|
|
4160
|
+
const session = parsed?.session?.[sid];
|
|
4161
|
+
const providerID = String(session?.model?.providerID || "").trim();
|
|
4162
|
+
const modelID = String(session?.model?.modelID || "").trim();
|
|
4163
|
+
if (providerID && modelID)
|
|
4164
|
+
return `${providerID}/${modelID}`;
|
|
4165
|
+
if (modelID)
|
|
4166
|
+
return modelID;
|
|
4167
|
+
} catch {
|
|
4168
|
+
}
|
|
4169
|
+
}
|
|
4170
|
+
} catch {
|
|
4171
|
+
}
|
|
4172
|
+
}
|
|
4173
|
+
return "";
|
|
4174
|
+
}
|
|
4175
|
+
function readLatestOpenCodeSessionId(directory3 = "") {
|
|
4176
|
+
try {
|
|
4177
|
+
const globalPath = join5(getOpenCodeDesktopHome(), "opencode.global.dat");
|
|
4178
|
+
if (!existsSync6(globalPath))
|
|
4179
|
+
return "";
|
|
4180
|
+
const st = statSync5(globalPath);
|
|
4181
|
+
if (!st.isFile() || st.size > 10485760)
|
|
4182
|
+
return "";
|
|
4183
|
+
const raw = safeJsonParse3(readFileSync5(globalPath, "utf-8"));
|
|
4184
|
+
const notifications = typeof raw?.notification === "string" ? safeJsonParse3(raw.notification) : raw?.notification;
|
|
4185
|
+
const list = Array.isArray(notifications?.list) ? notifications.list : [];
|
|
4186
|
+
const targetDir = directory3 ? resolve(directory3) : "";
|
|
4187
|
+
const rows = list.filter((entry) => {
|
|
4188
|
+
const entryDir = String(entry?.directory || "").trim();
|
|
4189
|
+
const session = String(entry?.session || "").trim();
|
|
4190
|
+
if (!entryDir || !session)
|
|
4191
|
+
return false;
|
|
4192
|
+
if (!targetDir)
|
|
4193
|
+
return true;
|
|
4194
|
+
try {
|
|
4195
|
+
return resolve(entryDir) === targetDir;
|
|
4196
|
+
} catch {
|
|
4197
|
+
return entryDir === targetDir;
|
|
4198
|
+
}
|
|
4199
|
+
});
|
|
4200
|
+
rows.sort((a, b) => Number(b?.time || 0) - Number(a?.time || 0));
|
|
4201
|
+
return String(rows[0]?.session || "").trim();
|
|
4119
4202
|
} catch {
|
|
4120
4203
|
return "";
|
|
4121
4204
|
}
|
|
@@ -4137,6 +4220,53 @@ function readOpenCodeConfigObject(dir) {
|
|
|
4137
4220
|
}
|
|
4138
4221
|
return {};
|
|
4139
4222
|
}
|
|
4223
|
+
function collectConfiguredProviderModelsFromConfig(cfg) {
|
|
4224
|
+
const out = [];
|
|
4225
|
+
const providers = cfg?.provider || {};
|
|
4226
|
+
for (const [providerName, providerCfg] of Object.entries(providers)) {
|
|
4227
|
+
const models = providerCfg?.models || {};
|
|
4228
|
+
for (const rawId of Object.keys(models)) {
|
|
4229
|
+
const id2 = String(rawId || "").trim();
|
|
4230
|
+
if (!id2)
|
|
4231
|
+
continue;
|
|
4232
|
+
out.push(id2.includes("/") ? id2 : `${providerName}/${id2}`);
|
|
4233
|
+
}
|
|
4234
|
+
}
|
|
4235
|
+
return out;
|
|
4236
|
+
}
|
|
4237
|
+
function resolveConfiguredModelId(model, configs = []) {
|
|
4238
|
+
const raw = String(model || "").trim();
|
|
4239
|
+
if (!raw)
|
|
4240
|
+
return "";
|
|
4241
|
+
if (raw.includes("/"))
|
|
4242
|
+
return raw;
|
|
4243
|
+
const normalized = normalizeModelId(raw);
|
|
4244
|
+
const matches = /* @__PURE__ */ new Set();
|
|
4245
|
+
for (const cfg of configs) {
|
|
4246
|
+
for (const id2 of collectConfiguredProviderModelsFromConfig(cfg)) {
|
|
4247
|
+
const bare = String(id2 || "").includes("/") ? String(id2).split("/").pop() : id2;
|
|
4248
|
+
if (normalizeModelId(id2) === normalized || normalizeModelId(bare) === normalized)
|
|
4249
|
+
matches.add(id2);
|
|
4250
|
+
}
|
|
4251
|
+
}
|
|
4252
|
+
return matches.size === 1 ? [...matches][0] : raw;
|
|
4253
|
+
}
|
|
4254
|
+
function resolveDisplayModelId(model, directory3 = "") {
|
|
4255
|
+
const raw = String(model || "").trim();
|
|
4256
|
+
if (!raw)
|
|
4257
|
+
return "";
|
|
4258
|
+
if (raw.includes("/"))
|
|
4259
|
+
return raw;
|
|
4260
|
+
const configs = [];
|
|
4261
|
+
const projectCfg = readOpenCodeConfigObject(directory3);
|
|
4262
|
+
if (projectCfg && typeof projectCfg === "object")
|
|
4263
|
+
configs.push(projectCfg);
|
|
4264
|
+
const homeDir = getOpenCodeHome();
|
|
4265
|
+
const homeCfg = readOpenCodeConfigObject(homeDir);
|
|
4266
|
+
if (homeCfg && typeof homeCfg === "object")
|
|
4267
|
+
configs.push(homeCfg);
|
|
4268
|
+
return resolveConfiguredModelId(raw, configs);
|
|
4269
|
+
}
|
|
4140
4270
|
function _setTrinitySlotsFromTiers(tiersData) {
|
|
4141
4271
|
const brain = String(tiersData?.trinity?.brain?.oc || "").trim();
|
|
4142
4272
|
const medium = String(tiersData?.trinity?.medium?.oc || "").trim();
|
|
@@ -6190,7 +6320,7 @@ function createTrinityTool(deps) {
|
|
|
6190
6320
|
}
|
|
6191
6321
|
const auth = deps._readAuth();
|
|
6192
6322
|
try {
|
|
6193
|
-
const ok = await deps.probeModel(targetModel, auth);
|
|
6323
|
+
const ok = await deps.probeModel(targetModel, auth, deps._loadOpenCodeProviders());
|
|
6194
6324
|
if (!ok)
|
|
6195
6325
|
console.error("[vibeOS] WARN: " + targetModel + " probe failed - switching anyway");
|
|
6196
6326
|
} catch (e) {
|
|
@@ -6770,7 +6900,7 @@ ${L.repeat(40)}`);
|
|
|
6770
6900
|
for (const id2 of candidates) {
|
|
6771
6901
|
if (probed.brain)
|
|
6772
6902
|
break;
|
|
6773
|
-
const ok = await deps.probeModel(id2, auth);
|
|
6903
|
+
const ok = await deps.probeModel(id2, auth, providers);
|
|
6774
6904
|
if (ok)
|
|
6775
6905
|
probed.brain = models.find((m) => m.id === id2) || { id: id2, cost: deps._modelCost(id2), tier: deps._modelTier(id2) };
|
|
6776
6906
|
else
|
|
@@ -6782,7 +6912,7 @@ ${L.repeat(40)}`);
|
|
|
6782
6912
|
break;
|
|
6783
6913
|
if (m.id === probed.brain?.id)
|
|
6784
6914
|
continue;
|
|
6785
|
-
const ok = await deps.probeModel(m.id, auth);
|
|
6915
|
+
const ok = await deps.probeModel(m.id, auth, providers);
|
|
6786
6916
|
if (ok)
|
|
6787
6917
|
probed.cheap = m;
|
|
6788
6918
|
else if (!failed.some((f) => f.endsWith(m.id)))
|
|
@@ -6793,7 +6923,7 @@ ${L.repeat(40)}`);
|
|
|
6793
6923
|
break;
|
|
6794
6924
|
if (id2 === probed.brain?.id || id2 === probed.cheap?.id)
|
|
6795
6925
|
continue;
|
|
6796
|
-
const ok = await deps.probeModel(id2, auth);
|
|
6926
|
+
const ok = await deps.probeModel(id2, auth, providers);
|
|
6797
6927
|
if (ok)
|
|
6798
6928
|
probed.medium = models.find((m) => m.id === id2) || { id: id2, cost: deps._modelCost(id2), tier: deps._modelTier(id2) };
|
|
6799
6929
|
else if (!failed.some((f) => f.endsWith(id2)))
|
|
@@ -6876,7 +7006,7 @@ ${L.repeat(40)}`);
|
|
|
6876
7006
|
if (deps.currentModel || !deps.existsSync(deps.TIERS_FILE)) {
|
|
6877
7007
|
try {
|
|
6878
7008
|
const auth = deps._readAuth();
|
|
6879
|
-
const ok = await deps.probeModel(deps.currentModel, auth);
|
|
7009
|
+
const ok = await deps.probeModel(deps.currentModel, auth, deps._loadOpenCodeProviders());
|
|
6880
7010
|
results.push({
|
|
6881
7011
|
ok,
|
|
6882
7012
|
okLabel: ok ? "\u2705" : "\u274C",
|
|
@@ -7154,6 +7284,50 @@ function normalizeProviderModels(providerName, models) {
|
|
|
7154
7284
|
}
|
|
7155
7285
|
return out;
|
|
7156
7286
|
}
|
|
7287
|
+
function resolveProviderModel(modelId, providers) {
|
|
7288
|
+
const raw = String(modelId || "").trim();
|
|
7289
|
+
if (!raw)
|
|
7290
|
+
return null;
|
|
7291
|
+
const normalized = normalizeModelId(raw);
|
|
7292
|
+
const entries = Object.entries(providers || {});
|
|
7293
|
+
for (const [providerName, providerCfg] of entries) {
|
|
7294
|
+
const ids = normalizeProviderModels(providerName, providerCfg?.models);
|
|
7295
|
+
for (const id2 of ids) {
|
|
7296
|
+
const bare = String(id2 || "").includes("/") ? String(id2).split("/").slice(1).join("/") : String(id2);
|
|
7297
|
+
if (normalizeModelId(id2) === normalized || normalizeModelId(bare) === normalized) {
|
|
7298
|
+
return { providerName, providerCfg, id: id2 };
|
|
7299
|
+
}
|
|
7300
|
+
}
|
|
7301
|
+
}
|
|
7302
|
+
const prefix = raw.includes("/") ? raw.split("/")[0] : "";
|
|
7303
|
+
if (prefix && providers?.[prefix]) {
|
|
7304
|
+
return { providerName: prefix, providerCfg: providers[prefix], id: raw };
|
|
7305
|
+
}
|
|
7306
|
+
return null;
|
|
7307
|
+
}
|
|
7308
|
+
function providerApiBaseURL(providerName, providerCfg) {
|
|
7309
|
+
const options = providerCfg?.options || {};
|
|
7310
|
+
const baseURL = String(options?.baseURL || options?.baseUrl || providerCfg?.baseURL || providerCfg?.baseUrl || providerCfg?.url || "").trim();
|
|
7311
|
+
if (baseURL)
|
|
7312
|
+
return baseURL.replace(/\/+$/, "");
|
|
7313
|
+
if (providerName === "deepseek")
|
|
7314
|
+
return "https://api.deepseek.com/v1";
|
|
7315
|
+
if (providerName === "openrouter")
|
|
7316
|
+
return "https://openrouter.ai/api/v1";
|
|
7317
|
+
if (providerName === "google")
|
|
7318
|
+
return "https://generativelanguage.googleapis.com/v1beta";
|
|
7319
|
+
return "";
|
|
7320
|
+
}
|
|
7321
|
+
function providerApiKey(providerName, providerCfg, auth) {
|
|
7322
|
+
const options = providerCfg?.options || {};
|
|
7323
|
+
const direct = String(options?.apiKey || providerCfg?.apiKey || providerCfg?.key || "").trim();
|
|
7324
|
+
if (direct)
|
|
7325
|
+
return direct;
|
|
7326
|
+
const scoped = String(auth?.[providerName]?.key || "").trim();
|
|
7327
|
+
if (scoped)
|
|
7328
|
+
return scoped;
|
|
7329
|
+
return "";
|
|
7330
|
+
}
|
|
7157
7331
|
function collectConfiguredProviderModels(providers) {
|
|
7158
7332
|
const all = [];
|
|
7159
7333
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -7306,40 +7480,47 @@ function modelToCcAlias(modelId) {
|
|
|
7306
7480
|
}
|
|
7307
7481
|
return "haiku";
|
|
7308
7482
|
}
|
|
7309
|
-
async function probeModel(modelId, auth) {
|
|
7483
|
+
async function probeModel(modelId, auth, providers = null) {
|
|
7310
7484
|
if (!modelId || !auth)
|
|
7311
7485
|
return true;
|
|
7312
7486
|
const id2 = String(modelId || "");
|
|
7313
7487
|
if (id2.startsWith("opencode/"))
|
|
7314
7488
|
return true;
|
|
7315
|
-
|
|
7316
|
-
|
|
7317
|
-
|
|
7318
|
-
|
|
7319
|
-
|
|
7320
|
-
|
|
7321
|
-
|
|
7322
|
-
apiKey = auth.openrouter?.key;
|
|
7323
|
-
reqModel = id2.replace("openrouter/", "");
|
|
7324
|
-
} else {
|
|
7489
|
+
const provider = resolveProviderModel(id2, providers);
|
|
7490
|
+
const providerName = provider?.providerName || (id2.includes("/") ? id2.split("/")[0] : "");
|
|
7491
|
+
const providerCfg = provider?.providerCfg || providers?.[providerName] || {};
|
|
7492
|
+
const reqModel = provider?.id ? provider.id.includes("/") ? provider.id.split("/").slice(1).join("/") : provider.id : id2.includes("/") ? id2.split("/").slice(1).join("/") : id2;
|
|
7493
|
+
const apiKey = providerApiKey(providerName, providerCfg, auth);
|
|
7494
|
+
const baseURL = providerApiBaseURL(providerName, providerCfg);
|
|
7495
|
+
if (!providerName || !reqModel) {
|
|
7325
7496
|
return true;
|
|
7326
7497
|
}
|
|
7327
7498
|
if (!apiKey) {
|
|
7328
7499
|
console.error("[vibeOS] probeModel: no API key for " + id2);
|
|
7329
7500
|
return false;
|
|
7330
7501
|
}
|
|
7502
|
+
if (!baseURL && providerName !== "google") {
|
|
7503
|
+
return true;
|
|
7504
|
+
}
|
|
7331
7505
|
try {
|
|
7506
|
+
const isGoogleDirect = providerName === "google" && !String(baseURL || "").includes("chat/completions");
|
|
7507
|
+
const apiUrl = isGoogleDirect ? `https://generativelanguage.googleapis.com/v1beta/models/${encodeURIComponent(reqModel)}:generateContent?key=${encodeURIComponent(apiKey)}` : `${baseURL || providerApiBaseURL(providerName, providerCfg) || ""}/chat/completions`;
|
|
7508
|
+
const headers = isGoogleDirect ? { "Content-Type": "application/json", "x-goog-api-key": apiKey } : {
|
|
7509
|
+
"Authorization": "Bearer " + apiKey,
|
|
7510
|
+
"Content-Type": "application/json"
|
|
7511
|
+
};
|
|
7512
|
+
const body = isGoogleDirect ? JSON.stringify({
|
|
7513
|
+
contents: [{ role: "user", parts: [{ text: "ok" }] }],
|
|
7514
|
+
generationConfig: { maxOutputTokens: 1 }
|
|
7515
|
+
}) : JSON.stringify({
|
|
7516
|
+
model: reqModel,
|
|
7517
|
+
messages: [{ role: "user", content: "ok" }],
|
|
7518
|
+
max_tokens: 1
|
|
7519
|
+
});
|
|
7332
7520
|
const res = await fetch(apiUrl, {
|
|
7333
7521
|
method: "POST",
|
|
7334
|
-
headers
|
|
7335
|
-
|
|
7336
|
-
"Content-Type": "application/json"
|
|
7337
|
-
},
|
|
7338
|
-
body: JSON.stringify({
|
|
7339
|
-
model: reqModel,
|
|
7340
|
-
messages: [{ role: "user", content: "ok" }],
|
|
7341
|
-
max_tokens: 1
|
|
7342
|
-
}),
|
|
7522
|
+
headers,
|
|
7523
|
+
body,
|
|
7343
7524
|
signal: AbortSignal.timeout(8e3)
|
|
7344
7525
|
});
|
|
7345
7526
|
if (!res.ok) {
|
|
@@ -8624,16 +8805,17 @@ async function _appendFooter(input, output, directory3) {
|
|
|
8624
8805
|
let _footerStress = 0;
|
|
8625
8806
|
if (latestUserIntent)
|
|
8626
8807
|
_footerStress = scoreStress(latestUserIntent);
|
|
8627
|
-
|
|
8628
|
-
|
|
8629
|
-
|
|
8630
|
-
|
|
8631
|
-
|
|
8632
|
-
|
|
8808
|
+
try {
|
|
8809
|
+
const cfg = await client.config.get("model");
|
|
8810
|
+
if (cfg) {
|
|
8811
|
+
const cfgModel = String(cfg);
|
|
8812
|
+
if (cfgModel !== currentModel) {
|
|
8813
|
+
setCurrentModel(cfgModel);
|
|
8814
|
+
setCurrentTier(classify(cfgModel));
|
|
8633
8815
|
console.error(`[vibeOS] client-detected model: ${currentModel} (tier=${currentTier})`);
|
|
8634
8816
|
}
|
|
8635
|
-
} catch {
|
|
8636
8817
|
}
|
|
8818
|
+
} catch {
|
|
8637
8819
|
}
|
|
8638
8820
|
try {
|
|
8639
8821
|
const messageID = input?.messageID || input?.messageId || input?.message?.id || output?.messageID || output?.messageId || output?.message?.id || null;
|
|
@@ -8650,12 +8832,23 @@ async function _appendFooter(input, output, directory3) {
|
|
|
8650
8832
|
const sessionSlot = loadSessionSlot(_OC_SID5);
|
|
8651
8833
|
const slot = sessionSlot || loadSelection3().active_slot || "brain";
|
|
8652
8834
|
const brainModel = slot === "brain" ? TRINITY_BRAIN || currentModel : slot === "medium" ? TRINITY_MEDIUM || currentModel : TRINITY_CHEAP || currentModel || "";
|
|
8653
|
-
let
|
|
8835
|
+
let liveModel = "";
|
|
8836
|
+
try {
|
|
8837
|
+
const cfg = await client.config.get("model");
|
|
8838
|
+
if (cfg)
|
|
8839
|
+
liveModel = String(cfg);
|
|
8840
|
+
} catch {
|
|
8841
|
+
}
|
|
8842
|
+
if (!liveModel) {
|
|
8843
|
+
liveModel = readConfig(directory3) || readConfig(join14(process.env.HOME || "", ".config", "opencode")) || process?.env?.OPENCODE_MODEL || "";
|
|
8844
|
+
}
|
|
8845
|
+
const displayModel = resolveDisplayModelId(liveModel || currentModel || brainModel || "", directory3) || liveModel || currentModel || brainModel;
|
|
8846
|
+
let modelTag = `[${shortModelName(displayModel)}]`;
|
|
8654
8847
|
const _workerModel = slot === "brain" ? TRINITY_MEDIUM : null;
|
|
8655
8848
|
const totalTurns = (sesModelTurns?.brain || 0) + (sesModelTurns?.worker || 0);
|
|
8656
8849
|
if (_workerModel && _workerModel !== brainModel) {
|
|
8657
8850
|
const brainPct = Math.round((sesModelTurns?.brain || 0) / (totalTurns || 1) * 100);
|
|
8658
|
-
modelTag = `[${shortModelName(
|
|
8851
|
+
modelTag = `[${shortModelName(displayModel)} ${brainPct}% \u2192 ${shortModelName(_workerModel)} ${100 - brainPct}%]`;
|
|
8659
8852
|
}
|
|
8660
8853
|
_autoReportCount = (_autoReportCount || 0) + 1;
|
|
8661
8854
|
if (_autoReportCount % 5 === 0) {
|
|
@@ -8718,19 +8911,9 @@ async function _appendFooter(input, output, directory3) {
|
|
|
8718
8911
|
if (stripped !== text)
|
|
8719
8912
|
return;
|
|
8720
8913
|
const ltTotal = ltTasks + ltCache;
|
|
8721
|
-
const modeVerbMap = {
|
|
8722
|
-
balanced: "routing",
|
|
8723
|
-
budget: "saving",
|
|
8724
|
-
quality: "focusing",
|
|
8725
|
-
speed: "moving",
|
|
8726
|
-
longrun: "pacing",
|
|
8727
|
-
auto: "vibing",
|
|
8728
|
-
"web-research": "researching",
|
|
8729
|
-
forensic: "investigating"
|
|
8730
|
-
};
|
|
8731
8914
|
const optMode = (resolvedMode || "budget").toLowerCase();
|
|
8732
|
-
const
|
|
8733
|
-
let vibeLine = `\u2014 ${flashIcon ? `${flashIcon} ` : ""}
|
|
8915
|
+
const modeLabel = optMode === "quality" ? "quality" : optMode === "speed" ? "speed" : optMode === "longrun" ? "longrun" : "";
|
|
8916
|
+
let vibeLine = `\u2014 ${flashIcon ? `${flashIcon} ` : ""}run: ${displayModel}`;
|
|
8734
8917
|
if (ltTotal > 0) {
|
|
8735
8918
|
vibeLine += ` | $${formatUsd(ltTotal)} saved`;
|
|
8736
8919
|
}
|
|
@@ -8742,6 +8925,8 @@ async function _appendFooter(input, output, directory3) {
|
|
|
8742
8925
|
} else if (problemStreak > 0) {
|
|
8743
8926
|
vibeLine += ` | recovery ${problemStreak}`;
|
|
8744
8927
|
}
|
|
8928
|
+
if (modeLabel)
|
|
8929
|
+
vibeLine += ` | ${modeLabel}`;
|
|
8745
8930
|
vibeLine += ` | VIBE${flashIcon ? " \u26A1" : ""}`;
|
|
8746
8931
|
if (_footerStress > 0.4) {
|
|
8747
8932
|
const stressLabel = _footerStress > 0.7 ? "high" : "elevated";
|
|
@@ -10652,8 +10837,19 @@ var onToolExecuteAfter = async (input, output) => {
|
|
|
10652
10837
|
stressTag = ` stress:${label}`;
|
|
10653
10838
|
}
|
|
10654
10839
|
}
|
|
10840
|
+
let liveModel = "";
|
|
10841
|
+
try {
|
|
10842
|
+
const cfg = await client.config.get("model");
|
|
10843
|
+
if (cfg)
|
|
10844
|
+
liveModel = String(cfg);
|
|
10845
|
+
} catch {
|
|
10846
|
+
}
|
|
10847
|
+
if (!liveModel) {
|
|
10848
|
+
liveModel = readConfig(projectDirectory) || readConfig(join16(process.env.HOME || "", ".config", "opencode")) || process?.env?.OPENCODE_MODEL || "";
|
|
10849
|
+
}
|
|
10850
|
+
const displayModel = resolveDisplayModelId(liveModel || currentModel || "", projectDirectory) || liveModel || currentModel;
|
|
10655
10851
|
if (ltTotal > 0) {
|
|
10656
|
-
_footerText = `\u2014 ${flashIcon ? `${flashIcon} ` : ""}
|
|
10852
|
+
_footerText = `\u2014 ${flashIcon ? `${flashIcon} ` : ""}run: ${displayModel} | $${formatUsd(ltTotal)} saved | VIBE${flashIcon ? " \u26A1" : ""} \u2014
|
|
10657
10853
|
|
|
10658
10854
|
`;
|
|
10659
10855
|
} else {
|
package/src/lib/hooks/footer.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// @ts-nocheck
|
|
2
2
|
import { readFileSync, appendFileSync, mkdirSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
import { classify, _refreshModel, TRINITY_BRAIN, TRINITY_MEDIUM, TRINITY_CHEAP, shortModelName, roundUsd, formatUsd } from "../pricing.js";
|
|
4
|
+
import { classify, _refreshModel, readConfig, resolveDisplayModelId, TRINITY_BRAIN, TRINITY_MEDIUM, TRINITY_CHEAP, shortModelName, roundUsd, formatUsd } from "../pricing.js";
|
|
5
5
|
import { latestUserIntent } from "./chat-transform.js";
|
|
6
6
|
import { scoreStress, resolveEnforcementMode, detectOutcomeSignal, getBlackboxTracker, syncOutcomeToApi, loadOptimizationMode, classifyTurnSimple } from "../turn-classify.js";
|
|
7
7
|
import { peekBudgetFirstMode, recordBudgetFirstOutcome } from "../mode-policy.js";
|
|
@@ -139,18 +139,19 @@ async function _appendFooter(input, output, directory) {
|
|
|
139
139
|
let _footerStress = 0;
|
|
140
140
|
if (latestUserIntent)
|
|
141
141
|
_footerStress = scoreStress(latestUserIntent);
|
|
142
|
-
//
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
142
|
+
// Always prefer the live OpenCode model setting when available.
|
|
143
|
+
try {
|
|
144
|
+
const cfg = await client.config.get("model");
|
|
145
|
+
if (cfg) {
|
|
146
|
+
const cfgModel = String(cfg);
|
|
147
|
+
if (cfgModel !== currentModel) {
|
|
148
|
+
setCurrentModel(cfgModel);
|
|
149
|
+
setCurrentTier(classify(cfgModel));
|
|
149
150
|
console.error(`[vibeOS] client-detected model: ${currentModel} (tier=${currentTier})`);
|
|
150
151
|
}
|
|
151
152
|
}
|
|
152
|
-
catch { /* client.config may not be available */ }
|
|
153
153
|
}
|
|
154
|
+
catch { /* client.config may not be available */ }
|
|
154
155
|
try {
|
|
155
156
|
const messageID = input?.messageID ||
|
|
156
157
|
input?.messageId ||
|
|
@@ -175,12 +176,23 @@ async function _appendFooter(input, output, directory) {
|
|
|
175
176
|
const sessionSlot = loadSessionSlot(_OC_SID);
|
|
176
177
|
const slot = sessionSlot || loadSelection().active_slot || "brain";
|
|
177
178
|
const brainModel = slot === "brain" ? (TRINITY_BRAIN || currentModel) : slot === "medium" ? (TRINITY_MEDIUM || currentModel) : (TRINITY_CHEAP || currentModel || "");
|
|
178
|
-
let
|
|
179
|
+
let liveModel = "";
|
|
180
|
+
try {
|
|
181
|
+
const cfg = await client.config.get("model");
|
|
182
|
+
if (cfg)
|
|
183
|
+
liveModel = String(cfg);
|
|
184
|
+
}
|
|
185
|
+
catch { }
|
|
186
|
+
if (!liveModel) {
|
|
187
|
+
liveModel = readConfig(directory) || readConfig(join(process.env.HOME || "", ".config", "opencode")) || process?.env?.OPENCODE_MODEL || "";
|
|
188
|
+
}
|
|
189
|
+
const displayModel = resolveDisplayModelId(liveModel || currentModel || brainModel || "", directory) || liveModel || currentModel || brainModel;
|
|
190
|
+
let modelTag = `[${shortModelName(displayModel)}]`;
|
|
179
191
|
const _workerModel = slot === "brain" ? TRINITY_MEDIUM : null;
|
|
180
192
|
const totalTurns = (sesModelTurns?.brain || 0) + (sesModelTurns?.worker || 0);
|
|
181
193
|
if (_workerModel && _workerModel !== brainModel) {
|
|
182
194
|
const brainPct = Math.round(((sesModelTurns?.brain || 0) / (totalTurns || 1)) * 100);
|
|
183
|
-
modelTag = `[${shortModelName(
|
|
195
|
+
modelTag = `[${shortModelName(displayModel)} ${brainPct}% → ${shortModelName(_workerModel)} ${100 - brainPct}%]`;
|
|
184
196
|
}
|
|
185
197
|
_autoReportCount = (_autoReportCount || 0) + 1;
|
|
186
198
|
if (_autoReportCount % 5 === 0) {
|
|
@@ -248,19 +260,9 @@ async function _appendFooter(input, output, directory) {
|
|
|
248
260
|
return;
|
|
249
261
|
const ltTotal = ltTasks + ltCache;
|
|
250
262
|
// Build dopamine footer
|
|
251
|
-
const modeVerbMap = {
|
|
252
|
-
balanced: 'routing',
|
|
253
|
-
budget: 'saving',
|
|
254
|
-
quality: 'focusing',
|
|
255
|
-
speed: 'moving',
|
|
256
|
-
longrun: 'pacing',
|
|
257
|
-
auto: 'vibing',
|
|
258
|
-
'web-research': 'researching',
|
|
259
|
-
forensic: 'investigating',
|
|
260
|
-
};
|
|
261
263
|
const optMode = (resolvedMode || 'budget').toLowerCase();
|
|
262
|
-
const
|
|
263
|
-
let vibeLine = `— ${flashIcon ? `${flashIcon} ` : ""}
|
|
264
|
+
const modeLabel = optMode === "quality" ? "quality" : optMode === "speed" ? "speed" : optMode === "longrun" ? "longrun" : "";
|
|
265
|
+
let vibeLine = `— ${flashIcon ? `${flashIcon} ` : ""}run: ${displayModel}`;
|
|
264
266
|
if (ltTotal > 0) {
|
|
265
267
|
vibeLine += ` | $${formatUsd(ltTotal)} saved`;
|
|
266
268
|
}
|
|
@@ -273,6 +275,8 @@ async function _appendFooter(input, output, directory) {
|
|
|
273
275
|
else if (problemStreak > 0) {
|
|
274
276
|
vibeLine += ` | recovery ${problemStreak}`;
|
|
275
277
|
}
|
|
278
|
+
if (modeLabel)
|
|
279
|
+
vibeLine += ` | ${modeLabel}`;
|
|
276
280
|
vibeLine += ` | VIBE${flashIcon ? ' ⚡' : ''}`;
|
|
277
281
|
if (_footerStress > 0.4) {
|
|
278
282
|
const stressLabel = _footerStress > 0.7 ? 'high' : 'elevated';
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { writeFileSync, appendFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
3
3
|
import { join, dirname, basename } from 'node:path';
|
|
4
4
|
import { currentTier, currentModel, setCurrentModel, setCurrentTier, _OC_SID, _modelLocked, loadSelection, readLifetimeSavings, recordCacheSaving, recordMissedContext7, getScratchpadHit, recordScratchpadObservation, recordPrivacyTelemetry, updateState, SAVINGS_LEDGER_FILE, CONTEXT7_INSTALL_FLAG, SOFT_QUOTA_LIMIT, upsertTodo, ML_ENABLED, _mlGraph, _cacheDb, _mlSavePending, ML_CONFIDENCE_THRESHOLD, setMlSavePending, saveMLState, SCRATCHPAD_TOOLS, applyDecadence, } from '../state.js';
|
|
5
|
-
import { classify, modelCostPerTurn, isModelFree, detectContext7, isDocsTarget, shortModelName, formatUsd, _refreshModel, TRINITY_CHEAP, TRINITY_MEDIUM, trendDisplay, modelToSlotLabel, } from '../pricing.js';
|
|
5
|
+
import { classify, modelCostPerTurn, isModelFree, detectContext7, isDocsTarget, shortModelName, formatUsd, _refreshModel, readConfig, resolveDisplayModelId, TRINITY_CHEAP, TRINITY_MEDIUM, trendDisplay, modelToSlotLabel, } from '../pricing.js';
|
|
6
6
|
import { latestUserIntent } from './chat-transform.js';
|
|
7
7
|
import { scoreStress, extractFirstWordFromArgs, shouldLogWarn, isUserAskingForTests, resolveEnforcementMode, getLearnedExploratoryWords, noteTaskRoutingLearning, } from '../turn-classify.js';
|
|
8
8
|
import { saveReport } from '../reporting.js';
|
|
@@ -604,8 +604,19 @@ export const onToolExecuteAfter = async (input, output) => {
|
|
|
604
604
|
stressTag = ` stress:${label}`;
|
|
605
605
|
}
|
|
606
606
|
}
|
|
607
|
+
let liveModel = "";
|
|
608
|
+
try {
|
|
609
|
+
const cfg = await client.config.get("model");
|
|
610
|
+
if (cfg)
|
|
611
|
+
liveModel = String(cfg);
|
|
612
|
+
}
|
|
613
|
+
catch { }
|
|
614
|
+
if (!liveModel) {
|
|
615
|
+
liveModel = readConfig(projectDirectory) || readConfig(join(process.env.HOME || "", ".config", "opencode")) || process?.env?.OPENCODE_MODEL || "";
|
|
616
|
+
}
|
|
617
|
+
const displayModel = resolveDisplayModelId(liveModel || currentModel || "", projectDirectory) || liveModel || currentModel;
|
|
607
618
|
if (ltTotal > 0) {
|
|
608
|
-
_footerText = `— ${flashIcon ? `${flashIcon} ` : ""}
|
|
619
|
+
_footerText = `— ${flashIcon ? `${flashIcon} ` : ""}run: ${displayModel} | $${formatUsd(ltTotal)} saved | VIBE${flashIcon ? " ⚡" : ""} —\n\n`;
|
|
609
620
|
}
|
|
610
621
|
else {
|
|
611
622
|
_footerText = `${statusLine}${stressTag}\n\n`;
|
package/src/lib/pricing.js
CHANGED
|
@@ -15,7 +15,7 @@ export function setTrinityCheap(v) { TRINITY_CHEAP = v; }
|
|
|
15
15
|
* context7 detection, per-turn cost estimation, and slot management.
|
|
16
16
|
*/
|
|
17
17
|
import { readFileSync, writeFileSync, appendFileSync, existsSync, mkdirSync, statSync, copyFileSync, renameSync, openSync, closeSync, rmSync, readdirSync } from "node:fs";
|
|
18
|
-
import { join, dirname, basename } from "node:path";
|
|
18
|
+
import { join, dirname, basename, resolve } from "node:path";
|
|
19
19
|
import { homedir, tmpdir } from "node:os";
|
|
20
20
|
import { createHash } from "node:crypto";
|
|
21
21
|
import { currentModel, currentTier, setCurrentModel, setCurrentTier, safeJsonParse, HIGH_TIER_RE, MID_TIER_RE, loadTierRegexes, _modelLocked } from "./state.js";
|
|
@@ -34,6 +34,9 @@ function getVibeOSHome() {
|
|
|
34
34
|
function getOpenCodeHome() {
|
|
35
35
|
return process.env.VIBEOS_OPENCODE_HOME || join(process.env.HOME || homedir(), ".config", "opencode");
|
|
36
36
|
}
|
|
37
|
+
function getOpenCodeDesktopHome() {
|
|
38
|
+
return process.env.VIBEOS_OPENCODE_DESKTOP_HOME || join(process.env.HOME || homedir(), "Library", "Application Support", "ai.opencode.desktop");
|
|
39
|
+
}
|
|
37
40
|
const TIERS_FILE = join(getVibeOSHome(), "model-tiers.json");
|
|
38
41
|
function _handleStateCorruption(path) {
|
|
39
42
|
const backupDir = join(getVibeOSHome(), ".backups");
|
|
@@ -502,8 +505,95 @@ function loadSelection() {
|
|
|
502
505
|
const DFLT_SEL = { enabled: true, active_slot: null, thinking_level: "off", flow_enabled: false, tdd_enforce: false, tdd_strict: false, tdd_quality: true, flow_enforce: false, delegation_enforce: true };
|
|
503
506
|
export function readConfig(dir) {
|
|
504
507
|
try {
|
|
505
|
-
const
|
|
506
|
-
|
|
508
|
+
const configs = [];
|
|
509
|
+
const workspaceModel = readWorkspaceSessionModel(dir);
|
|
510
|
+
if (workspaceModel)
|
|
511
|
+
return workspaceModel;
|
|
512
|
+
const projectCfg = readOpenCodeConfigObject(dir);
|
|
513
|
+
if (projectCfg && typeof projectCfg === "object")
|
|
514
|
+
configs.push(projectCfg);
|
|
515
|
+
const homeDir = getOpenCodeHome();
|
|
516
|
+
if (dir !== homeDir) {
|
|
517
|
+
const homeCfg = readOpenCodeConfigObject(homeDir);
|
|
518
|
+
if (homeCfg && typeof homeCfg === "object")
|
|
519
|
+
configs.push(homeCfg);
|
|
520
|
+
}
|
|
521
|
+
const selectedCfg = configs[0] || {};
|
|
522
|
+
const selectedModel = selectedCfg?.agent?.build?.model || selectedCfg?.model || "";
|
|
523
|
+
return resolveConfiguredModelId(selectedModel, configs);
|
|
524
|
+
}
|
|
525
|
+
catch {
|
|
526
|
+
return "";
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
function readWorkspaceSessionModel(directory = "") {
|
|
530
|
+
const sid = readLatestOpenCodeSessionId(directory);
|
|
531
|
+
if (!sid)
|
|
532
|
+
return "";
|
|
533
|
+
const roots = [getOpenCodeDesktopHome(), getOpenCodeHome()];
|
|
534
|
+
for (const root of roots) {
|
|
535
|
+
try {
|
|
536
|
+
if (!existsSync(root) || !statSync(root).isDirectory())
|
|
537
|
+
continue;
|
|
538
|
+
const files = readdirSync(root)
|
|
539
|
+
.filter((name) => /^opencode\.workspace\..*\.dat$/i.test(name))
|
|
540
|
+
.map((name) => join(root, name))
|
|
541
|
+
.sort((a, b) => statSync(b).mtimeMs - statSync(a).mtimeMs);
|
|
542
|
+
for (const file of files) {
|
|
543
|
+
try {
|
|
544
|
+
const raw = readFileSync(file, "utf-8");
|
|
545
|
+
if (!raw.includes(sid) || !raw.includes("workspace:model-selection"))
|
|
546
|
+
continue;
|
|
547
|
+
const match = raw.match(/"workspace:model-selection"\s*:\s*"((?:\\.|[^"\\])*)"/s);
|
|
548
|
+
if (!match)
|
|
549
|
+
continue;
|
|
550
|
+
const decoded = JSON.parse(`"${match[1]}"`);
|
|
551
|
+
const parsed = safeJsonParse(decoded);
|
|
552
|
+
const session = parsed?.session?.[sid];
|
|
553
|
+
const providerID = String(session?.model?.providerID || "").trim();
|
|
554
|
+
const modelID = String(session?.model?.modelID || "").trim();
|
|
555
|
+
if (providerID && modelID)
|
|
556
|
+
return `${providerID}/${modelID}`;
|
|
557
|
+
if (modelID)
|
|
558
|
+
return modelID;
|
|
559
|
+
}
|
|
560
|
+
catch { }
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
catch { }
|
|
564
|
+
}
|
|
565
|
+
return "";
|
|
566
|
+
}
|
|
567
|
+
function readLatestOpenCodeSessionId(directory = "") {
|
|
568
|
+
try {
|
|
569
|
+
const globalPath = join(getOpenCodeDesktopHome(), "opencode.global.dat");
|
|
570
|
+
if (!existsSync(globalPath))
|
|
571
|
+
return "";
|
|
572
|
+
const st = statSync(globalPath);
|
|
573
|
+
if (!st.isFile() || st.size > 10485760)
|
|
574
|
+
return "";
|
|
575
|
+
const raw = safeJsonParse(readFileSync(globalPath, "utf-8"));
|
|
576
|
+
const notifications = typeof raw?.notification === "string"
|
|
577
|
+
? safeJsonParse(raw.notification)
|
|
578
|
+
: raw?.notification;
|
|
579
|
+
const list = Array.isArray(notifications?.list) ? notifications.list : [];
|
|
580
|
+
const targetDir = directory ? resolve(directory) : "";
|
|
581
|
+
const rows = list.filter((entry) => {
|
|
582
|
+
const entryDir = String(entry?.directory || "").trim();
|
|
583
|
+
const session = String(entry?.session || "").trim();
|
|
584
|
+
if (!entryDir || !session)
|
|
585
|
+
return false;
|
|
586
|
+
if (!targetDir)
|
|
587
|
+
return true;
|
|
588
|
+
try {
|
|
589
|
+
return resolve(entryDir) === targetDir;
|
|
590
|
+
}
|
|
591
|
+
catch {
|
|
592
|
+
return entryDir === targetDir;
|
|
593
|
+
}
|
|
594
|
+
});
|
|
595
|
+
rows.sort((a, b) => Number(b?.time || 0) - Number(a?.time || 0));
|
|
596
|
+
return String(rows[0]?.session || "").trim();
|
|
507
597
|
}
|
|
508
598
|
catch {
|
|
509
599
|
return "";
|
|
@@ -526,6 +616,53 @@ function readOpenCodeConfigObject(dir) {
|
|
|
526
616
|
}
|
|
527
617
|
return {};
|
|
528
618
|
}
|
|
619
|
+
function collectConfiguredProviderModelsFromConfig(cfg) {
|
|
620
|
+
const out = [];
|
|
621
|
+
const providers = cfg?.provider || {};
|
|
622
|
+
for (const [providerName, providerCfg] of Object.entries(providers)) {
|
|
623
|
+
const models = providerCfg?.models || {};
|
|
624
|
+
for (const rawId of Object.keys(models)) {
|
|
625
|
+
const id = String(rawId || "").trim();
|
|
626
|
+
if (!id)
|
|
627
|
+
continue;
|
|
628
|
+
out.push(id.includes("/") ? id : `${providerName}/${id}`);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
return out;
|
|
632
|
+
}
|
|
633
|
+
function resolveConfiguredModelId(model, configs = []) {
|
|
634
|
+
const raw = String(model || "").trim();
|
|
635
|
+
if (!raw)
|
|
636
|
+
return "";
|
|
637
|
+
if (raw.includes("/"))
|
|
638
|
+
return raw;
|
|
639
|
+
const normalized = normalizeModelId(raw);
|
|
640
|
+
const matches = new Set();
|
|
641
|
+
for (const cfg of configs) {
|
|
642
|
+
for (const id of collectConfiguredProviderModelsFromConfig(cfg)) {
|
|
643
|
+
const bare = String(id || "").includes("/") ? String(id).split("/").pop() : id;
|
|
644
|
+
if (normalizeModelId(id) === normalized || normalizeModelId(bare) === normalized)
|
|
645
|
+
matches.add(id);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
return matches.size === 1 ? [...matches][0] : raw;
|
|
649
|
+
}
|
|
650
|
+
export function resolveDisplayModelId(model, directory = "") {
|
|
651
|
+
const raw = String(model || "").trim();
|
|
652
|
+
if (!raw)
|
|
653
|
+
return "";
|
|
654
|
+
if (raw.includes("/"))
|
|
655
|
+
return raw;
|
|
656
|
+
const configs = [];
|
|
657
|
+
const projectCfg = readOpenCodeConfigObject(directory);
|
|
658
|
+
if (projectCfg && typeof projectCfg === "object")
|
|
659
|
+
configs.push(projectCfg);
|
|
660
|
+
const homeDir = getOpenCodeHome();
|
|
661
|
+
const homeCfg = readOpenCodeConfigObject(homeDir);
|
|
662
|
+
if (homeCfg && typeof homeCfg === "object")
|
|
663
|
+
configs.push(homeCfg);
|
|
664
|
+
return resolveConfiguredModelId(raw, configs);
|
|
665
|
+
}
|
|
529
666
|
function _setTrinitySlotsFromTiers(tiersData) {
|
|
530
667
|
const brain = String(tiersData?.trinity?.brain?.oc || "").trim();
|
|
531
668
|
const medium = String(tiersData?.trinity?.medium?.oc || "").trim();
|
|
@@ -50,6 +50,50 @@ function normalizeProviderModels(providerName, models) {
|
|
|
50
50
|
}
|
|
51
51
|
return out;
|
|
52
52
|
}
|
|
53
|
+
function resolveProviderModel(modelId, providers) {
|
|
54
|
+
const raw = String(modelId || "").trim();
|
|
55
|
+
if (!raw)
|
|
56
|
+
return null;
|
|
57
|
+
const normalized = normalizeModelId(raw);
|
|
58
|
+
const entries = Object.entries(providers || {});
|
|
59
|
+
for (const [providerName, providerCfg] of entries) {
|
|
60
|
+
const ids = normalizeProviderModels(providerName, providerCfg?.models);
|
|
61
|
+
for (const id of ids) {
|
|
62
|
+
const bare = String(id || "").includes("/") ? String(id).split("/").slice(1).join("/") : String(id);
|
|
63
|
+
if (normalizeModelId(id) === normalized || normalizeModelId(bare) === normalized) {
|
|
64
|
+
return { providerName, providerCfg, id };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const prefix = raw.includes("/") ? raw.split("/")[0] : "";
|
|
69
|
+
if (prefix && providers?.[prefix]) {
|
|
70
|
+
return { providerName: prefix, providerCfg: providers[prefix], id: raw };
|
|
71
|
+
}
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
function providerApiBaseURL(providerName, providerCfg) {
|
|
75
|
+
const options = providerCfg?.options || {};
|
|
76
|
+
const baseURL = String(options?.baseURL || options?.baseUrl || providerCfg?.baseURL || providerCfg?.baseUrl || providerCfg?.url || "").trim();
|
|
77
|
+
if (baseURL)
|
|
78
|
+
return baseURL.replace(/\/+$/, "");
|
|
79
|
+
if (providerName === "deepseek")
|
|
80
|
+
return "https://api.deepseek.com/v1";
|
|
81
|
+
if (providerName === "openrouter")
|
|
82
|
+
return "https://openrouter.ai/api/v1";
|
|
83
|
+
if (providerName === "google")
|
|
84
|
+
return "https://generativelanguage.googleapis.com/v1beta";
|
|
85
|
+
return "";
|
|
86
|
+
}
|
|
87
|
+
function providerApiKey(providerName, providerCfg, auth) {
|
|
88
|
+
const options = providerCfg?.options || {};
|
|
89
|
+
const direct = String(options?.apiKey || providerCfg?.apiKey || providerCfg?.key || "").trim();
|
|
90
|
+
if (direct)
|
|
91
|
+
return direct;
|
|
92
|
+
const scoped = String(auth?.[providerName]?.key || "").trim();
|
|
93
|
+
if (scoped)
|
|
94
|
+
return scoped;
|
|
95
|
+
return "";
|
|
96
|
+
}
|
|
53
97
|
export function collectConfiguredProviderModels(providers) {
|
|
54
98
|
const all = [];
|
|
55
99
|
const seen = new Set();
|
|
@@ -232,42 +276,53 @@ export function modelToCcAlias(modelId) {
|
|
|
232
276
|
}
|
|
233
277
|
return "haiku";
|
|
234
278
|
}
|
|
235
|
-
export async function probeModel(modelId, auth) {
|
|
279
|
+
export async function probeModel(modelId, auth, providers = null) {
|
|
236
280
|
if (!modelId || !auth)
|
|
237
281
|
return true;
|
|
238
282
|
const id = String(modelId || "");
|
|
239
283
|
if (id.startsWith("opencode/"))
|
|
240
284
|
return true;
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
apiUrl = "https://openrouter.ai/api/v1/chat/completions";
|
|
249
|
-
apiKey = auth.openrouter?.key;
|
|
250
|
-
reqModel = id.replace("openrouter/", "");
|
|
251
|
-
}
|
|
252
|
-
else {
|
|
285
|
+
const provider = resolveProviderModel(id, providers);
|
|
286
|
+
const providerName = provider?.providerName || (id.includes("/") ? id.split("/")[0] : "");
|
|
287
|
+
const providerCfg = provider?.providerCfg || providers?.[providerName] || {};
|
|
288
|
+
const reqModel = provider?.id ? (provider.id.includes("/") ? provider.id.split("/").slice(1).join("/") : provider.id) : (id.includes("/") ? id.split("/").slice(1).join("/") : id);
|
|
289
|
+
const apiKey = providerApiKey(providerName, providerCfg, auth);
|
|
290
|
+
const baseURL = providerApiBaseURL(providerName, providerCfg);
|
|
291
|
+
if (!providerName || !reqModel) {
|
|
253
292
|
return true;
|
|
254
293
|
}
|
|
255
294
|
if (!apiKey) {
|
|
256
295
|
console.error("[vibeOS] probeModel: no API key for " + id);
|
|
257
296
|
return false;
|
|
258
297
|
}
|
|
298
|
+
if (!baseURL && providerName !== "google") {
|
|
299
|
+
return true;
|
|
300
|
+
}
|
|
259
301
|
try {
|
|
260
|
-
const
|
|
261
|
-
|
|
262
|
-
|
|
302
|
+
const isGoogleDirect = providerName === "google" && !String(baseURL || "").includes("chat/completions");
|
|
303
|
+
const apiUrl = isGoogleDirect
|
|
304
|
+
? `https://generativelanguage.googleapis.com/v1beta/models/${encodeURIComponent(reqModel)}:generateContent?key=${encodeURIComponent(apiKey)}`
|
|
305
|
+
: `${baseURL || providerApiBaseURL(providerName, providerCfg) || ""}/chat/completions`;
|
|
306
|
+
const headers = isGoogleDirect
|
|
307
|
+
? { "Content-Type": "application/json", "x-goog-api-key": apiKey }
|
|
308
|
+
: {
|
|
263
309
|
"Authorization": "Bearer " + apiKey,
|
|
264
310
|
"Content-Type": "application/json",
|
|
265
|
-
}
|
|
266
|
-
|
|
311
|
+
};
|
|
312
|
+
const body = isGoogleDirect
|
|
313
|
+
? JSON.stringify({
|
|
314
|
+
contents: [{ role: "user", parts: [{ text: "ok" }] }],
|
|
315
|
+
generationConfig: { maxOutputTokens: 1 },
|
|
316
|
+
})
|
|
317
|
+
: JSON.stringify({
|
|
267
318
|
model: reqModel,
|
|
268
319
|
messages: [{ role: "user", content: "ok" }],
|
|
269
320
|
max_tokens: 1,
|
|
270
|
-
})
|
|
321
|
+
});
|
|
322
|
+
const res = await fetch(apiUrl, {
|
|
323
|
+
method: "POST",
|
|
324
|
+
headers,
|
|
325
|
+
body,
|
|
271
326
|
signal: AbortSignal.timeout(8000),
|
|
272
327
|
});
|
|
273
328
|
if (!res.ok) {
|
package/src/lib/trinity-tool.js
CHANGED
|
@@ -142,7 +142,7 @@ export function createTrinityTool(deps) {
|
|
|
142
142
|
}
|
|
143
143
|
const auth = deps._readAuth();
|
|
144
144
|
try {
|
|
145
|
-
const ok = await deps.probeModel(targetModel, auth);
|
|
145
|
+
const ok = await deps.probeModel(targetModel, auth, deps._loadOpenCodeProviders());
|
|
146
146
|
if (!ok)
|
|
147
147
|
console.error("[vibeOS] WARN: " + targetModel + " probe failed - switching anyway");
|
|
148
148
|
}
|
|
@@ -730,7 +730,7 @@ export function createTrinityTool(deps) {
|
|
|
730
730
|
for (const id of candidates) {
|
|
731
731
|
if (probed.brain)
|
|
732
732
|
break;
|
|
733
|
-
const ok = await deps.probeModel(id, auth);
|
|
733
|
+
const ok = await deps.probeModel(id, auth, providers);
|
|
734
734
|
if (ok)
|
|
735
735
|
probed.brain = models.find(m => m.id === id) || { id, cost: deps._modelCost(id), tier: deps._modelTier(id) };
|
|
736
736
|
else
|
|
@@ -742,7 +742,7 @@ export function createTrinityTool(deps) {
|
|
|
742
742
|
break;
|
|
743
743
|
if (m.id === probed.brain?.id)
|
|
744
744
|
continue;
|
|
745
|
-
const ok = await deps.probeModel(m.id, auth);
|
|
745
|
+
const ok = await deps.probeModel(m.id, auth, providers);
|
|
746
746
|
if (ok)
|
|
747
747
|
probed.cheap = m;
|
|
748
748
|
else if (!failed.some(f => f.endsWith(m.id)))
|
|
@@ -753,7 +753,7 @@ export function createTrinityTool(deps) {
|
|
|
753
753
|
break;
|
|
754
754
|
if (id === probed.brain?.id || id === probed.cheap?.id)
|
|
755
755
|
continue;
|
|
756
|
-
const ok = await deps.probeModel(id, auth);
|
|
756
|
+
const ok = await deps.probeModel(id, auth, providers);
|
|
757
757
|
if (ok)
|
|
758
758
|
probed.medium = models.find(m => m.id === id) || { id, cost: deps._modelCost(id), tier: deps._modelTier(id) };
|
|
759
759
|
else if (!failed.some(f => f.endsWith(id)))
|
|
@@ -838,7 +838,7 @@ export function createTrinityTool(deps) {
|
|
|
838
838
|
if (deps.currentModel || !deps.existsSync(deps.TIERS_FILE)) {
|
|
839
839
|
try {
|
|
840
840
|
const auth = deps._readAuth();
|
|
841
|
-
const ok = await deps.probeModel(deps.currentModel, auth);
|
|
841
|
+
const ok = await deps.probeModel(deps.currentModel, auth, deps._loadOpenCodeProviders());
|
|
842
842
|
results.push({
|
|
843
843
|
ok, okLabel: ok ? "\u2705" : "\u274c",
|
|
844
844
|
label: "model probe",
|