calvyn-code 0.14.3 → 0.14.5

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.
@@ -2136,7 +2136,7 @@ def _is_rate_limit_error(exc: Exception) -> bool:
2136
2136
  return False
2137
2137
 
2138
2138
 
2139
- def _is_connection_error(exc: Exception) -> bool:
2139
+ def _is_connection_error(exc: Exception) -> bool:
2140
2140
  """Detect connection/network errors that warrant provider fallback.
2141
2141
 
2142
2142
  Returns True for errors indicating the provider endpoint is unreachable
@@ -2170,8 +2170,21 @@ def _is_connection_error(exc: Exception) -> bool:
2170
2170
  "remoteprotocolerror",
2171
2171
  "localprotocolerror",
2172
2172
  )):
2173
- return True
2174
- return False
2173
+ return True
2174
+ return False
2175
+
2176
+
2177
+ def _is_not_found_error(exc: Exception) -> bool:
2178
+ """Detect provider 404s that should fall back for auto auxiliary tasks."""
2179
+ status = getattr(exc, "status_code", None)
2180
+ if status == 404:
2181
+ return True
2182
+ err_lower = str(exc).lower()
2183
+ return (
2184
+ "http 404" in err_lower
2185
+ or "404 not found" in err_lower
2186
+ or "status code: 404" in err_lower
2187
+ )
2175
2188
 
2176
2189
 
2177
2190
  def _is_auth_error(exc: Exception) -> bool:
@@ -4374,12 +4387,13 @@ def call_llm(
4374
4387
  # payment / auth chains below using the temperature-stripped
4375
4388
  # kwargs. Re-raise only if the retry hit something those
4376
4389
  # chains won't handle.
4377
- if not (
4378
- _is_payment_error(retry_err)
4379
- or _is_connection_error(retry_err)
4380
- or _is_auth_error(retry_err)
4381
- or "max_tokens" in retry_err_str
4382
- or "unsupported_parameter" in retry_err_str
4390
+ if not (
4391
+ _is_payment_error(retry_err)
4392
+ or _is_connection_error(retry_err)
4393
+ or _is_not_found_error(retry_err)
4394
+ or _is_auth_error(retry_err)
4395
+ or "max_tokens" in retry_err_str
4396
+ or "unsupported_parameter" in retry_err_str
4383
4397
  ):
4384
4398
  raise
4385
4399
  first_err = retry_err
@@ -4409,9 +4423,9 @@ def call_llm(
4409
4423
  except Exception as retry_err:
4410
4424
  # If the max_tokens retry also hits a payment or connection
4411
4425
  # error, fall through to the fallback chain below.
4412
- if not (_is_payment_error(retry_err) or _is_connection_error(retry_err) or _is_rate_limit_error(retry_err)):
4413
- raise
4414
- first_err = retry_err
4426
+ if not (_is_payment_error(retry_err) or _is_connection_error(retry_err) or _is_not_found_error(retry_err) or _is_rate_limit_error(retry_err)):
4427
+ raise
4428
+ first_err = retry_err
4415
4429
 
4416
4430
  # ── Nous auth refresh parity with main agent ──────────────────
4417
4431
  client_is_nous = (
@@ -4515,10 +4529,11 @@ def call_llm(
4515
4529
  # back to an alternative provider instead of exhausting retries
4516
4530
  # against the same rate-limited endpoint.
4517
4531
  should_fallback = (
4518
- _is_payment_error(first_err)
4519
- or _is_connection_error(first_err)
4520
- or _is_rate_limit_error(first_err)
4521
- )
4532
+ _is_payment_error(first_err)
4533
+ or _is_connection_error(first_err)
4534
+ or _is_not_found_error(first_err)
4535
+ or _is_rate_limit_error(first_err)
4536
+ )
4522
4537
  # Only try alternative providers when the user didn't explicitly
4523
4538
  # configure this task's provider. Explicit provider = hard constraint;
4524
4539
  # auto (the default) = best-effort fallback chain. (#7559)
@@ -4533,10 +4548,12 @@ def call_llm(
4533
4548
  _mark_provider_unhealthy(
4534
4549
  _recoverable_pool_provider(resolved_provider, client) or resolved_provider
4535
4550
  )
4536
- elif _is_rate_limit_error(first_err):
4537
- reason = "rate limit"
4538
- else:
4539
- reason = "connection error"
4551
+ elif _is_rate_limit_error(first_err):
4552
+ reason = "rate limit"
4553
+ elif _is_not_found_error(first_err):
4554
+ reason = "not found"
4555
+ else:
4556
+ reason = "connection error"
4540
4557
  logger.info("Auxiliary %s: %s on %s (%s), trying fallback",
4541
4558
  task or "call", reason, resolved_provider, first_err)
4542
4559
  fb_client, fb_model, fb_label = _try_payment_fallback(
@@ -4725,12 +4742,13 @@ async def async_call_llm(
4725
4742
  await client.chat.completions.create(**retry_kwargs), task)
4726
4743
  except Exception as retry_err:
4727
4744
  retry_err_str = str(retry_err)
4728
- if not (
4729
- _is_payment_error(retry_err)
4730
- or _is_connection_error(retry_err)
4731
- or _is_auth_error(retry_err)
4732
- or "max_tokens" in retry_err_str
4733
- or "unsupported_parameter" in retry_err_str
4745
+ if not (
4746
+ _is_payment_error(retry_err)
4747
+ or _is_connection_error(retry_err)
4748
+ or _is_not_found_error(retry_err)
4749
+ or _is_auth_error(retry_err)
4750
+ or "max_tokens" in retry_err_str
4751
+ or "unsupported_parameter" in retry_err_str
4734
4752
  ):
4735
4753
  raise
4736
4754
  first_err = retry_err
@@ -4760,9 +4778,9 @@ async def async_call_llm(
4760
4778
  except Exception as retry_err:
4761
4779
  # If the max_tokens retry also hits a payment or connection
4762
4780
  # error, fall through to the fallback chain below.
4763
- if not (_is_payment_error(retry_err) or _is_connection_error(retry_err) or _is_rate_limit_error(retry_err)):
4764
- raise
4765
- first_err = retry_err
4781
+ if not (_is_payment_error(retry_err) or _is_connection_error(retry_err) or _is_not_found_error(retry_err) or _is_rate_limit_error(retry_err)):
4782
+ raise
4783
+ first_err = retry_err
4766
4784
 
4767
4785
  # ── Nous auth refresh parity with main agent ──────────────────
4768
4786
  client_is_nous = (
@@ -4847,10 +4865,11 @@ async def async_call_llm(
4847
4865
 
4848
4866
  # ── Payment / connection / rate-limit fallback (mirrors sync call_llm) ──
4849
4867
  should_fallback = (
4850
- _is_payment_error(first_err)
4851
- or _is_connection_error(first_err)
4852
- or _is_rate_limit_error(first_err)
4853
- )
4868
+ _is_payment_error(first_err)
4869
+ or _is_connection_error(first_err)
4870
+ or _is_not_found_error(first_err)
4871
+ or _is_rate_limit_error(first_err)
4872
+ )
4854
4873
  is_auto = resolved_provider in {"auto", "", None}
4855
4874
  if should_fallback and is_auto:
4856
4875
  if _is_payment_error(first_err):
@@ -4858,10 +4877,12 @@ async def async_call_llm(
4858
4877
  _mark_provider_unhealthy(
4859
4878
  _recoverable_pool_provider(resolved_provider, client) or resolved_provider
4860
4879
  )
4861
- elif _is_rate_limit_error(first_err):
4862
- reason = "rate limit"
4863
- else:
4864
- reason = "connection error"
4880
+ elif _is_rate_limit_error(first_err):
4881
+ reason = "rate limit"
4882
+ elif _is_not_found_error(first_err):
4883
+ reason = "not found"
4884
+ else:
4885
+ reason = "connection error"
4865
4886
  logger.info("Auxiliary %s (async): %s on %s (%s), trying fallback",
4866
4887
  task or "call", reason, resolved_provider, first_err)
4867
4888
  fb_client, fb_model, fb_label = _try_payment_fallback(
package/bin/calvyn.js CHANGED
@@ -18,6 +18,12 @@ function resolveCalvynBinary() {
18
18
  return path.join(packageRoot, ".venv", venvName, filename)
19
19
  }
20
20
 
21
+ function resolveVenvPython() {
22
+ return isWindows
23
+ ? path.join(packageRoot, ".venv", "Scripts", "python.exe")
24
+ : path.join(packageRoot, ".venv", "bin", "python")
25
+ }
26
+
21
27
  function runPostinstallIfNeeded() {
22
28
  const calvynBinary = resolveCalvynBinary()
23
29
  if (fs.existsSync(calvynBinary)) {
@@ -50,12 +56,15 @@ function runPostinstallIfNeeded() {
50
56
  }
51
57
 
52
58
  const args = process.argv.slice(2)
53
- const calvynBinary = runPostinstallIfNeeded()
54
- const result = spawnSync(calvynBinary, args, {
59
+ runPostinstallIfNeeded()
60
+ const venvPython = resolveVenvPython()
61
+ const cliEntry = path.join(packageRoot, "cli.py")
62
+ const result = spawnSync(venvPython, [cliEntry, ...args], {
55
63
  stdio: "inherit",
56
64
  shell: false,
57
65
  env: {
58
66
  ...process.env,
67
+ CALVYN_LAUNCHED_FROM_NPM: "",
59
68
  CALVYN_REPO_ROOT: packageRoot,
60
69
  CALVYN_HOME: process.env.CALVYN_HOME || path.join(require("os").homedir(), ".calvyn"),
61
70
  },
package/cli.py CHANGED
@@ -6959,7 +6959,13 @@ class HermesCLI:
6959
6959
  try:
6960
6960
  if ctx is None:
6961
6961
  raise RuntimeError("inventory context unavailable")
6962
- providers = build_models_payload(ctx, max_models=50)["providers"]
6962
+ providers = build_models_payload(
6963
+ ctx,
6964
+ include_unconfigured=True,
6965
+ picker_hints=True,
6966
+ canonical_order=True,
6967
+ max_models=50,
6968
+ )["providers"]
6963
6969
  except Exception:
6964
6970
  providers = []
6965
6971
 
@@ -157,27 +157,31 @@ def build_models_payload(
157
157
  # ─── Internal: row post-processing ──────────────────────────────────────
158
158
 
159
159
 
160
- def _append_unconfigured_rows(rows: list[dict], ctx: ConfigContext) -> list[dict]:
161
- """Build skeleton rows for canonical providers missing from ``rows``."""
162
- from hermes_cli.models import CANONICAL_PROVIDERS, _PROVIDER_LABELS
160
+ def _append_unconfigured_rows(rows: list[dict], ctx: ConfigContext) -> list[dict]:
161
+ """Build skeleton rows for canonical providers missing from ``rows``."""
162
+ from hermes_cli.models import CANONICAL_PROVIDERS, OPENROUTER_MODELS, _PROVIDER_LABELS, _PROVIDER_MODELS
163
163
 
164
164
  seen = {r["slug"].lower() for r in rows}
165
165
  cur = (ctx.current_provider or "").lower()
166
166
  extras: list[dict] = []
167
- for entry in CANONICAL_PROVIDERS:
168
- if entry.slug.lower() in seen:
169
- continue
170
- extras.append(
171
- {
172
- "slug": entry.slug,
173
- "name": _PROVIDER_LABELS.get(entry.slug, entry.label),
174
- "is_current": entry.slug.lower() == cur,
175
- "is_user_defined": False,
176
- "models": [],
177
- "total_models": 0,
178
- "source": "canonical",
179
- }
180
- )
167
+ for entry in CANONICAL_PROVIDERS:
168
+ if entry.slug.lower() in seen:
169
+ continue
170
+ if entry.slug == "openrouter":
171
+ models = [mid for mid, _ in OPENROUTER_MODELS]
172
+ else:
173
+ models = list(_PROVIDER_MODELS.get(entry.slug, []))
174
+ extras.append(
175
+ {
176
+ "slug": entry.slug,
177
+ "name": _PROVIDER_LABELS.get(entry.slug, entry.label),
178
+ "is_current": entry.slug.lower() == cur,
179
+ "is_user_defined": False,
180
+ "models": models,
181
+ "total_models": len(models),
182
+ "source": "canonical",
183
+ }
184
+ )
181
185
  return extras
182
186
 
183
187
 
@@ -196,10 +200,10 @@ def _apply_picker_hints(rows: list[dict]) -> None:
196
200
  continue
197
201
  # Distinguish authenticated rows (returned by
198
202
  # list_authenticated_providers) from skeleton rows (from
199
- # _append_unconfigured_rows). The skeleton rows have empty
200
- # `models` AND source="canonical"; authenticated rows have
201
- # populated `models` OR a non-canonical source.
202
- is_skeleton = row.get("source") == "canonical" and not row.get("models")
203
+ # _append_unconfigured_rows). Skeleton rows come from source="canonical";
204
+ # they may still carry static fallback models so the picker can show
205
+ # choices before credentials are configured.
206
+ is_skeleton = row.get("source") == "canonical"
203
207
  row["authenticated"] = not is_skeleton
204
208
  if not is_skeleton or row.get("is_user_defined"):
205
209
  continue
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "calvyn-code",
3
- "version": "0.14.3",
3
+ "version": "0.14.5",
4
4
  "description": "Calvyn Code — AI агент с инструментами, мессенджерами и локальным CLI",
5
5
  "bin": {
6
6
  "calvyn": "./bin/calvyn.js",
package/run_agent.py CHANGED
@@ -3210,12 +3210,32 @@ class AIAgent:
3210
3210
  except Exception:
3211
3211
  pass
3212
3212
 
3213
- def _emit_auxiliary_failure(self, task: str, exc: BaseException) -> None:
3214
- """Surface a compact warning for failed auxiliary work."""
3215
- try:
3216
- detail = self._summarize_api_error(exc)
3217
- except Exception:
3218
- detail = str(exc)
3213
+ def _emit_auxiliary_failure(self, task: str, exc: BaseException) -> None:
3214
+ """Surface a compact warning for failed auxiliary work."""
3215
+ try:
3216
+ from agent.auxiliary_client import (
3217
+ _is_not_found_error,
3218
+ _is_payment_error,
3219
+ _is_rate_limit_error,
3220
+ )
3221
+ except Exception:
3222
+ _is_payment_error = _is_rate_limit_error = _is_not_found_error = None
3223
+
3224
+ if task == "title generation":
3225
+ try:
3226
+ if (
3227
+ (_is_payment_error and _is_payment_error(exc))
3228
+ or (_is_rate_limit_error and _is_rate_limit_error(exc))
3229
+ or (_is_not_found_error and _is_not_found_error(exc))
3230
+ ):
3231
+ logger.debug("Suppressing auxiliary %s warning: %s", task, exc)
3232
+ return
3233
+ except Exception:
3234
+ pass
3235
+ try:
3236
+ detail = self._summarize_api_error(exc)
3237
+ except Exception:
3238
+ detail = str(exc)
3219
3239
  detail = (detail or exc.__class__.__name__).strip()
3220
3240
  if len(detail) > 220:
3221
3241
  detail = detail[:217].rstrip() + "..."