vibeostheog 0.19.9 → 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 +6 -0
- package/README.md +10 -10
- package/package.json +1 -1
- package/src/index.js +220 -39
- package/src/lib/hooks/footer.js +3 -3
- package/src/lib/hooks/tool-execute.js +3 -3
- 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,9 @@
|
|
|
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
|
+
|
|
1
7
|
## 0.19.9
|
|
2
8
|
- fix: show live run model in footer
|
|
3
9
|
|
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");
|
|
@@ -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) {
|
|
@@ -8661,7 +8842,7 @@ async function _appendFooter(input, output, directory3) {
|
|
|
8661
8842
|
if (!liveModel) {
|
|
8662
8843
|
liveModel = readConfig(directory3) || readConfig(join14(process.env.HOME || "", ".config", "opencode")) || process?.env?.OPENCODE_MODEL || "";
|
|
8663
8844
|
}
|
|
8664
|
-
const displayModel = liveModel || currentModel || brainModel;
|
|
8845
|
+
const displayModel = resolveDisplayModelId(liveModel || currentModel || brainModel || "", directory3) || liveModel || currentModel || brainModel;
|
|
8665
8846
|
let modelTag = `[${shortModelName(displayModel)}]`;
|
|
8666
8847
|
const _workerModel = slot === "brain" ? TRINITY_MEDIUM : null;
|
|
8667
8848
|
const totalTurns = (sesModelTurns?.brain || 0) + (sesModelTurns?.worker || 0);
|
|
@@ -8732,7 +8913,7 @@ async function _appendFooter(input, output, directory3) {
|
|
|
8732
8913
|
const ltTotal = ltTasks + ltCache;
|
|
8733
8914
|
const optMode = (resolvedMode || "budget").toLowerCase();
|
|
8734
8915
|
const modeLabel = optMode === "quality" ? "quality" : optMode === "speed" ? "speed" : optMode === "longrun" ? "longrun" : "";
|
|
8735
|
-
let vibeLine = `\u2014 ${flashIcon ? `${flashIcon} ` : ""}run: ${
|
|
8916
|
+
let vibeLine = `\u2014 ${flashIcon ? `${flashIcon} ` : ""}run: ${displayModel}`;
|
|
8736
8917
|
if (ltTotal > 0) {
|
|
8737
8918
|
vibeLine += ` | $${formatUsd(ltTotal)} saved`;
|
|
8738
8919
|
}
|
|
@@ -10666,9 +10847,9 @@ var onToolExecuteAfter = async (input, output) => {
|
|
|
10666
10847
|
if (!liveModel) {
|
|
10667
10848
|
liveModel = readConfig(projectDirectory) || readConfig(join16(process.env.HOME || "", ".config", "opencode")) || process?.env?.OPENCODE_MODEL || "";
|
|
10668
10849
|
}
|
|
10669
|
-
const displayModel = liveModel || currentModel;
|
|
10850
|
+
const displayModel = resolveDisplayModelId(liveModel || currentModel || "", projectDirectory) || liveModel || currentModel;
|
|
10670
10851
|
if (ltTotal > 0) {
|
|
10671
|
-
_footerText = `\u2014 ${flashIcon ? `${flashIcon} ` : ""}run: ${
|
|
10852
|
+
_footerText = `\u2014 ${flashIcon ? `${flashIcon} ` : ""}run: ${displayModel} | $${formatUsd(ltTotal)} saved | VIBE${flashIcon ? " \u26A1" : ""} \u2014
|
|
10672
10853
|
|
|
10673
10854
|
`;
|
|
10674
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, readConfig, 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";
|
|
@@ -186,7 +186,7 @@ async function _appendFooter(input, output, directory) {
|
|
|
186
186
|
if (!liveModel) {
|
|
187
187
|
liveModel = readConfig(directory) || readConfig(join(process.env.HOME || "", ".config", "opencode")) || process?.env?.OPENCODE_MODEL || "";
|
|
188
188
|
}
|
|
189
|
-
const displayModel = liveModel || currentModel || brainModel;
|
|
189
|
+
const displayModel = resolveDisplayModelId(liveModel || currentModel || brainModel || "", directory) || liveModel || currentModel || brainModel;
|
|
190
190
|
let modelTag = `[${shortModelName(displayModel)}]`;
|
|
191
191
|
const _workerModel = slot === "brain" ? TRINITY_MEDIUM : null;
|
|
192
192
|
const totalTurns = (sesModelTurns?.brain || 0) + (sesModelTurns?.worker || 0);
|
|
@@ -262,7 +262,7 @@ async function _appendFooter(input, output, directory) {
|
|
|
262
262
|
// Build dopamine footer
|
|
263
263
|
const optMode = (resolvedMode || 'budget').toLowerCase();
|
|
264
264
|
const modeLabel = optMode === "quality" ? "quality" : optMode === "speed" ? "speed" : optMode === "longrun" ? "longrun" : "";
|
|
265
|
-
let vibeLine = `— ${flashIcon ? `${flashIcon} ` : ""}run: ${
|
|
265
|
+
let vibeLine = `— ${flashIcon ? `${flashIcon} ` : ""}run: ${displayModel}`;
|
|
266
266
|
if (ltTotal > 0) {
|
|
267
267
|
vibeLine += ` | $${formatUsd(ltTotal)} saved`;
|
|
268
268
|
}
|
|
@@ -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, readConfig, 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';
|
|
@@ -614,9 +614,9 @@ export const onToolExecuteAfter = async (input, output) => {
|
|
|
614
614
|
if (!liveModel) {
|
|
615
615
|
liveModel = readConfig(projectDirectory) || readConfig(join(process.env.HOME || "", ".config", "opencode")) || process?.env?.OPENCODE_MODEL || "";
|
|
616
616
|
}
|
|
617
|
-
const displayModel = liveModel || currentModel;
|
|
617
|
+
const displayModel = resolveDisplayModelId(liveModel || currentModel || "", projectDirectory) || liveModel || currentModel;
|
|
618
618
|
if (ltTotal > 0) {
|
|
619
|
-
_footerText = `— ${flashIcon ? `${flashIcon} ` : ""}run: ${
|
|
619
|
+
_footerText = `— ${flashIcon ? `${flashIcon} ` : ""}run: ${displayModel} | $${formatUsd(ltTotal)} saved | VIBE${flashIcon ? " ⚡" : ""} —\n\n`;
|
|
620
620
|
}
|
|
621
621
|
else {
|
|
622
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",
|