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 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 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.8",
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");
@@ -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": 0.00013,
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 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) {
@@ -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
- if (!currentModel) {
8628
- try {
8629
- const cfg = await client.config.get("model");
8630
- if (cfg) {
8631
- setCurrentModel(String(cfg));
8632
- setCurrentTier(classify(String(cfg)));
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 modelTag = `[${shortModelName(brainModel)}]`;
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(brainModel)} ${brainPct}% \u2192 ${shortModelName(_workerModel)} ${100 - brainPct}%]`;
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 modeVerb = modeVerbMap[optMode] || "vibing";
8733
- let vibeLine = `\u2014 ${flashIcon ? `${flashIcon} ` : ""}${modeVerb} on ${shortModelName(brainModel)}`;
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} ` : ""}saving on ${shortModelName(currentModel)} | $${formatUsd(ltTotal)} saved | VIBE${flashIcon ? " \u26A1" : ""} \u2014
10852
+ _footerText = `\u2014 ${flashIcon ? `${flashIcon} ` : ""}run: ${displayModel} | $${formatUsd(ltTotal)} saved | VIBE${flashIcon ? " \u26A1" : ""} \u2014
10657
10853
 
10658
10854
  `;
10659
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, 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
- // Lazy model detection: try client API once
143
- if (!currentModel) {
144
- try {
145
- const cfg = await client.config.get("model");
146
- if (cfg) {
147
- setCurrentModel(String(cfg));
148
- setCurrentTier(classify(String(cfg)));
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 modelTag = `[${shortModelName(brainModel)}]`;
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(brainModel)} ${brainPct}% → ${shortModelName(_workerModel)} ${100 - brainPct}%]`;
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 modeVerb = modeVerbMap[optMode] || 'vibing';
263
- let vibeLine = `— ${flashIcon ? `${flashIcon} ` : ""}${modeVerb} on ${shortModelName(brainModel)}`;
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} ` : ""}saving on ${shortModelName(currentModel)} | $${formatUsd(ltTotal)} saved | VIBE${flashIcon ? " ⚡" : ""} —\n\n`;
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`;
@@ -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",