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 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 Release** This is the first alpha milestone of vibeOS. See [CHANGELOG.md](CHANGELOG.md) for release notes.
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
- It also adds guardrails: delegation enforcement, flow and TDD controls, pattern learning, stress-aware routing, blackbox decision tracking, reporting, and remote API protection for the core algorithms.
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
- ## Why Teams Use It
9
+ ## What We Offer
10
10
 
11
- - Routes work to the right model tier without manual babysitting
12
- - Tracks delegation savings and cache savings separately
13
- - Shows live status in chat, the footer, and the web dashboard
14
- - Adds runtime controls for flow, TDD, model locking, and blackbox mode
15
- - Falls back to local algorithms if the remote API is unavailable
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 split
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.19.9",
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((resolve, reject) => {
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
- resolve({});
576
+ resolve2({});
577
577
  return;
578
578
  }
579
579
  try {
580
- resolve(JSON.parse(raw));
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((resolve, reject) => {
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
- resolve(srv);
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((resolve, reject) => {
848
- server2?.close((err) => err ? reject(err) : resolve());
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 c = readOpenCodeConfigObject(dir);
4118
- return c?.agent?.build?.model || c?.model || "";
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
- let apiUrl, apiKey, reqModel;
7316
- if (id2.startsWith("deepseek/")) {
7317
- apiUrl = "https://api.deepseek.com/chat/completions";
7318
- apiKey = auth.deepseek?.key;
7319
- reqModel = id2.replace("deepseek/", "");
7320
- } else if (id2.startsWith("openrouter/")) {
7321
- apiUrl = "https://openrouter.ai/api/v1/chat/completions";
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
- "Authorization": "Bearer " + apiKey,
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: ${shortModelName(displayModel)}`;
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: ${shortModelName(displayModel)} | $${formatUsd(ltTotal)} saved | VIBE${flashIcon ? " \u26A1" : ""} \u2014
10852
+ _footerText = `\u2014 ${flashIcon ? `${flashIcon} ` : ""}run: ${displayModel} | $${formatUsd(ltTotal)} saved | VIBE${flashIcon ? " \u26A1" : ""} \u2014
10672
10853
 
10673
10854
  `;
10674
10855
  } else {
@@ -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: ${shortModelName(displayModel)}`;
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: ${shortModelName(displayModel)} | $${formatUsd(ltTotal)} saved | VIBE${flashIcon ? " ⚡" : ""} —\n\n`;
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`;
@@ -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 c = readOpenCodeConfigObject(dir);
506
- return c?.agent?.build?.model || c?.model || "";
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
- let apiUrl, apiKey, reqModel;
242
- if (id.startsWith("deepseek/")) {
243
- apiUrl = "https://api.deepseek.com/chat/completions";
244
- apiKey = auth.deepseek?.key;
245
- reqModel = id.replace("deepseek/", "");
246
- }
247
- else if (id.startsWith("openrouter/")) {
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 res = await fetch(apiUrl, {
261
- method: "POST",
262
- headers: {
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
- body: JSON.stringify({
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) {
@@ -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",