corethink 1.0.2__tar.gz → 1.0.4__tar.gz

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.
Files changed (68) hide show
  1. {corethink-1.0.2 → corethink-1.0.4}/PKG-INFO +1 -1
  2. {corethink-1.0.2 → corethink-1.0.4}/pyproject.toml +1 -1
  3. {corethink-1.0.2 → corethink-1.0.4}/src/corethink/_version.py +1 -1
  4. {corethink-1.0.2 → corethink-1.0.4}/src/corethink/commands/endpoint.py +28 -9
  5. {corethink-1.0.2 → corethink-1.0.4}/src/corethink/commands/train.py +21 -3
  6. {corethink-1.0.2 → corethink-1.0.4}/src/corethink.egg-info/PKG-INFO +1 -1
  7. {corethink-1.0.2 → corethink-1.0.4}/tests/test_endpoint.py +30 -1
  8. {corethink-1.0.2 → corethink-1.0.4}/tests/test_train.py +44 -0
  9. {corethink-1.0.2 → corethink-1.0.4}/README.md +0 -0
  10. {corethink-1.0.2 → corethink-1.0.4}/setup.cfg +0 -0
  11. {corethink-1.0.2 → corethink-1.0.4}/src/corethink/__init__.py +0 -0
  12. {corethink-1.0.2 → corethink-1.0.4}/src/corethink/__main__.py +0 -0
  13. {corethink-1.0.2 → corethink-1.0.4}/src/corethink/account.py +0 -0
  14. {corethink-1.0.2 → corethink-1.0.4}/src/corethink/cli.py +0 -0
  15. {corethink-1.0.2 → corethink-1.0.4}/src/corethink/client.py +0 -0
  16. {corethink-1.0.2 → corethink-1.0.4}/src/corethink/commands/__init__.py +0 -0
  17. {corethink-1.0.2 → corethink-1.0.4}/src/corethink/commands/admin.py +0 -0
  18. {corethink-1.0.2 → corethink-1.0.4}/src/corethink/commands/api.py +0 -0
  19. {corethink-1.0.2 → corethink-1.0.4}/src/corethink/commands/archive.py +0 -0
  20. {corethink-1.0.2 → corethink-1.0.4}/src/corethink/commands/auth.py +0 -0
  21. {corethink-1.0.2 → corethink-1.0.4}/src/corethink/commands/billing.py +0 -0
  22. {corethink-1.0.2 → corethink-1.0.4}/src/corethink/commands/capture.py +0 -0
  23. {corethink-1.0.2 → corethink-1.0.4}/src/corethink/commands/connection.py +0 -0
  24. {corethink-1.0.2 → corethink-1.0.4}/src/corethink/commands/env.py +0 -0
  25. {corethink-1.0.2 → corethink-1.0.4}/src/corethink/commands/hub.py +0 -0
  26. {corethink-1.0.2 → corethink-1.0.4}/src/corethink/commands/library.py +0 -0
  27. {corethink-1.0.2 → corethink-1.0.4}/src/corethink/commands/models.py +0 -0
  28. {corethink-1.0.2 → corethink-1.0.4}/src/corethink/commands/workspace.py +0 -0
  29. {corethink-1.0.2 → corethink-1.0.4}/src/corethink/config.py +0 -0
  30. {corethink-1.0.2 → corethink-1.0.4}/src/corethink/context.py +0 -0
  31. {corethink-1.0.2 → corethink-1.0.4}/src/corethink/env_template.py +0 -0
  32. {corethink-1.0.2 → corethink-1.0.4}/src/corethink/environment_http_store.py +0 -0
  33. {corethink-1.0.2 → corethink-1.0.4}/src/corethink/experiment_cli.py +0 -0
  34. {corethink-1.0.2 → corethink-1.0.4}/src/corethink/hosted_profiles.py +0 -0
  35. {corethink-1.0.2 → corethink-1.0.4}/src/corethink/hosted_rl.py +0 -0
  36. {corethink-1.0.2 → corethink-1.0.4}/src/corethink/hosted_rl_archive.py +0 -0
  37. {corethink-1.0.2 → corethink-1.0.4}/src/corethink/hosted_rl_attribution.py +0 -0
  38. {corethink-1.0.2 → corethink-1.0.4}/src/corethink/hosted_rl_billing.py +0 -0
  39. {corethink-1.0.2 → corethink-1.0.4}/src/corethink/hosted_rl_state.py +0 -0
  40. {corethink-1.0.2 → corethink-1.0.4}/src/corethink/jobs_cli.py +0 -0
  41. {corethink-1.0.2 → corethink-1.0.4}/src/corethink/main.py +0 -0
  42. {corethink-1.0.2 → corethink-1.0.4}/src/corethink/output.py +0 -0
  43. {corethink-1.0.2 → corethink-1.0.4}/src/corethink/py.typed +0 -0
  44. {corethink-1.0.2 → corethink-1.0.4}/src/corethink/rl_cli.py +0 -0
  45. {corethink-1.0.2 → corethink-1.0.4}/src/corethink/rl_inspect.py +0 -0
  46. {corethink-1.0.2 → corethink-1.0.4}/src/corethink/rl_playground.py +0 -0
  47. {corethink-1.0.2 → corethink-1.0.4}/src/corethink/rl_reassessment.py +0 -0
  48. {corethink-1.0.2 → corethink-1.0.4}/src/corethink/rl_settings.py +0 -0
  49. {corethink-1.0.2 → corethink-1.0.4}/src/corethink/sdk.py +0 -0
  50. {corethink-1.0.2 → corethink-1.0.4}/src/corethink/settings.py +0 -0
  51. {corethink-1.0.2 → corethink-1.0.4}/src/corethink.egg-info/SOURCES.txt +0 -0
  52. {corethink-1.0.2 → corethink-1.0.4}/src/corethink.egg-info/dependency_links.txt +0 -0
  53. {corethink-1.0.2 → corethink-1.0.4}/src/corethink.egg-info/entry_points.txt +0 -0
  54. {corethink-1.0.2 → corethink-1.0.4}/src/corethink.egg-info/requires.txt +0 -0
  55. {corethink-1.0.2 → corethink-1.0.4}/src/corethink.egg-info/top_level.txt +0 -0
  56. {corethink-1.0.2 → corethink-1.0.4}/tests/test_admin.py +0 -0
  57. {corethink-1.0.2 → corethink-1.0.4}/tests/test_archive.py +0 -0
  58. {corethink-1.0.2 → corethink-1.0.4}/tests/test_auth.py +0 -0
  59. {corethink-1.0.2 → corethink-1.0.4}/tests/test_billing.py +0 -0
  60. {corethink-1.0.2 → corethink-1.0.4}/tests/test_capture.py +0 -0
  61. {corethink-1.0.2 → corethink-1.0.4}/tests/test_connection.py +0 -0
  62. {corethink-1.0.2 → corethink-1.0.4}/tests/test_env.py +0 -0
  63. {corethink-1.0.2 → corethink-1.0.4}/tests/test_hub.py +0 -0
  64. {corethink-1.0.2 → corethink-1.0.4}/tests/test_library.py +0 -0
  65. {corethink-1.0.2 → corethink-1.0.4}/tests/test_main_json.py +0 -0
  66. {corethink-1.0.2 → corethink-1.0.4}/tests/test_models.py +0 -0
  67. {corethink-1.0.2 → corethink-1.0.4}/tests/test_version.py +0 -0
  68. {corethink-1.0.2 → corethink-1.0.4}/tests/test_workspace.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: corethink
3
- Version: 1.0.2
3
+ Version: 1.0.4
4
4
  Summary: The CoreThink Workbench from your terminal: environments, training, endpoints and more.
5
5
  Author-email: CoreThink <support@corethink.ai>
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "corethink"
7
- version = "1.0.2"
7
+ version = "1.0.4"
8
8
  description = "The CoreThink Workbench from your terminal: environments, training, endpoints and more."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -1,3 +1,3 @@
1
1
  """The version of the `corethink` package (read by the build and by `corethink --version`)."""
2
2
 
3
- __version__ = "1.0.2"
3
+ __version__ = "1.0.4"
@@ -291,7 +291,7 @@ def _canary(percent: float | None, minutes: int | None) -> dict[str, Any]:
291
291
  def list_() -> None:
292
292
  """List every environment's endpoint and what staging and production serve.
293
293
 
294
- One row per environment in your workspace: its name and ID, the version on production and on staging (a dash when nothing is released there), and the model name to call. Below the table: how many endpoints serve a program, and the gateway's base URL. Use it to find an environment's ID and model name.
294
+ One row per environment in your workspace: its name and ID, the version on production and on staging (a dash when nothing is released there), and the model name to call. Below the table: how many endpoints serve a program, and the gateway's base URL. "unavailable" means that endpoint could not be read just then; the reason is printed below, and trying again usually works. Use it to find an environment's ID and model name.
295
295
 
296
296
  With `--json`: `gateway` (the base URL) and `endpoints`, each with `id`, `name`, `program`, `production`, `staging` and `error`. Next: `corethink endpoint show ENV`.
297
297
 
@@ -354,6 +354,11 @@ def list_() -> None:
354
354
  if rows:
355
355
  live = sum(1 for r in rows if r["production"] or r["staging"])
356
356
  ui.info(f"{live} of {len(rows)} serving a program · base URL {base} · model <program>@<alias>")
357
+ failed = [r for r in rows if r["error"] and not r["staging"]]
358
+ if failed:
359
+ ui.warn(
360
+ f"Could not read {len(failed)} endpoint{'' if len(failed) == 1 else 's'} just now ({failed[0]['error']}). Try again."
361
+ )
357
362
 
358
363
  ui.emit({"gateway": base, "endpoints": rows}, human)
359
364
 
@@ -937,7 +942,7 @@ def keys_usage(
937
942
  ) -> None:
938
943
  """Show requests, tokens and cost per day for your endpoint keys.
939
944
 
940
- Every key of the workspace by default; one key (give its ID or name) or one environment's keys (`--env`). It shows the last 30 days, or fewer with `--days`, and the totals below.
945
+ Every key of the workspace by default; one key (give its ID or name) or one environment's keys (`--env`). The table lists the days with requests in the last 30 days (or fewer with `--days`), and the totals below.
941
946
 
942
947
  With `--json`: `daily` (each day's `at`, `requests`, `tokens` and `cost`) and `total`.
943
948
 
@@ -969,14 +974,14 @@ def keys_usage(
969
974
 
970
975
  def human() -> None:
971
976
  ui.table(
972
- rows,
977
+ [r for r in rows if int(r.get("requests") or 0) > 0],
973
978
  [
974
979
  ("Day", lambda r: str(r.get("at") or "")[:10]),
975
980
  ("Requests", lambda r: f"{int(r.get('requests') or 0):,}"),
976
981
  ("Tokens", lambda r: f"{int(r.get('tokens') or 0):,}"),
977
982
  ("Cost", lambda r: ui.usd(r.get("cost"), 4)),
978
983
  ],
979
- empty="No usage yet.",
984
+ empty=f"No requests in the last {days} days.",
980
985
  )
981
986
  ui.info(
982
987
  f"{scope}, last {days} days: {total['requests']:,} requests · {total['tokens']:,} tokens · {ui.usd(total['cost'])}"
@@ -1057,7 +1062,7 @@ def call(
1057
1062
  ) -> None:
1058
1063
  """Send one chat completion to an endpoint and print the answer.
1059
1064
 
1060
- Sends your message (after `--system` and the earlier turns in `--messages`, with the tools from `--tools`) to the program on staging (the default), on production (`--alias production`) or on a fixed version, and prints its answer, any tool calls it asks for, and which version answered. The program asks for tool calls the way any OpenAI model does; your code runs them. Use it to try a program before you wire it into your app.
1065
+ Sends your message (after `--system` and the earlier turns in `--messages`, with the tools from `--tools`) to the program on staging (the default), on production (`--alias production`) or on a fixed version, and prints its answer, any tool calls it asks for, and which version answered. The program asks for tool calls the way any OpenAI model does; your code runs them. If the environment has tools, pass them (`--tools usecase.json`): without them the program answers without your data, and the command warns you. Use it to try a program before you wire it into your app.
1061
1066
 
1062
1067
  Calling needs only an endpoint key, from `CORETHINK_API_KEY` (make one with `corethink endpoint keys create ENV`). Give the program as `<workspace>/<environment>`, optionally with `@alias`, to skip the Workbench lookup (no sign-in needed). With `--json`: the whole response, including which program version answered (`corethink_program`).
1063
1068
 
@@ -1095,12 +1100,14 @@ def call(
1095
1100
  ui.error('Give a message: corethink endpoint call ENV "your message".')
1096
1101
  raise typer.Exit(2)
1097
1102
 
1098
- program, given_alias, _env = _target(env)
1103
+ program, given_alias, found = _target(env)
1099
1104
  model = f"{program}@{given_alias or alias}"
1100
1105
  base = _gateway_base(gateway_url)
1101
1106
  body: dict[str, Any] = {"model": model, "messages": chat}
1102
1107
  if tools_file:
1103
1108
  body["tools"] = _tools_from(tools_file)
1109
+ elif found and found.get("tools"):
1110
+ ui.warn(_tools_hint(found, "Without them it answers without your data: pass them with --tools usecase.json"))
1104
1111
  try:
1105
1112
  with (
1106
1113
  httpx.Client(
@@ -1155,6 +1162,14 @@ def call(
1155
1162
  ui.emit(answer, human)
1156
1163
 
1157
1164
 
1165
+ def _tools_hint(env: dict[str, Any], then: str) -> str:
1166
+ names = ", ".join(env.get("tools") or [])
1167
+ return (
1168
+ f"This program works with tools ({names}). {then} "
1169
+ f"(`corethink env cat {env.get('id')} usecase.json > usecase.json` has them)."
1170
+ )
1171
+
1172
+
1158
1173
  def _snippets(base: str, model: str) -> dict[str, str]:
1159
1174
  """The console's examples (Account.tsx apiSnippets), for this endpoint."""
1160
1175
  curl = (
@@ -1194,7 +1209,9 @@ def snippet(
1194
1209
 
1195
1210
  A curl command (the default) or a Python script with the OpenAI client (`--lang python`), with this endpoint's base URL and model name filled in. Nothing is called. Set `CORETHINK_API_KEY` to a key of this endpoint before you run it. Streaming is not supported yet.
1196
1211
 
1197
- With `--json`: `model`, `base_url`, and both examples (`curl` and `python`).
1212
+ If the environment has tools, it also names them and where to get their definitions (usecase.json's `tools`).
1213
+
1214
+ With `--json`: `model`, `base_url`, both examples (`curl` and `python`), and `tools` (the environment's tool names).
1198
1215
 
1199
1216
  Examples:
1200
1217
  corethink endpoint snippet refunds --lang python > call.py
@@ -1204,11 +1221,11 @@ def snippet(
1204
1221
  if lang not in ("curl", "python"):
1205
1222
  ui.error("--lang is curl or python.")
1206
1223
  raise typer.Exit(2)
1207
- program, given_alias, _env = _target(env)
1224
+ program, given_alias, found = _target(env)
1208
1225
  model = f"{program}@{given_alias or alias}"
1209
1226
  base = _gateway_base(gateway_url)
1210
1227
  snippets = _snippets(base, model)
1211
- data = {"model": model, "base_url": base, **snippets}
1228
+ data = {"model": model, "base_url": base, **snippets, "tools": (found or {}).get("tools") or []}
1212
1229
 
1213
1230
  def human() -> None:
1214
1231
  sys.stdout.write(snippets[lang] + "\n")
@@ -1216,6 +1233,8 @@ def snippet(
1216
1233
  ui.info(
1217
1234
  "Set CORETHINK_API_KEY to a key of this endpoint (`corethink endpoint keys create`). Streaming is not supported yet."
1218
1235
  )
1236
+ if found and found.get("tools"):
1237
+ ui.info(_tools_hint(found, "Send them as `tools`; your code runs each call and sends back its result"))
1219
1238
 
1220
1239
  ui.emit(data, human)
1221
1240
 
@@ -1021,6 +1021,12 @@ def credit_available() -> tuple[float | None, dict[str, Any] | None]:
1021
1021
  return (float(available) if available is not None else None), me.get("welcomeCredit")
1022
1022
 
1023
1023
 
1024
+ def likely_cost(estimate: dict[str, Any] | None) -> float | None:
1025
+ """What a run is likely to cost: from past runs, else the first-run estimate (a Hub copy's: what its Hub program cost)."""
1026
+ estimate = estimate or {}
1027
+ return estimate.get("costUsd") or (estimate.get("firstRun") or {}).get("typicalUsd")
1028
+
1029
+
1024
1030
  def default_budget(estimate_usd: Any, available: float | None, most: float | None, least: float = 1) -> float:
1025
1031
  """About 1.5 times the estimate (whole dollars), else $20; never above the most or the credit (Training.tsx)."""
1026
1032
  wanted = math.ceil(float(estimate_usd) * 1.5) if estimate_usd and float(estimate_usd) > 0 else FIRST_RUN_BUDGET
@@ -1075,6 +1081,8 @@ def estimate_lines(estimate: dict[str, Any] | None, budget: float | None = None)
1075
1081
  text += " The limit is lower; it may stop early."
1076
1082
  elif estimate.get("costUsd") is None and first and (first.get("lowUsd") or 0) > budget:
1077
1083
  text += " The limit is below the low end; it will likely stop early."
1084
+ elif estimate.get("costUsd") is None and first and (first.get("typicalUsd") or 0) > budget:
1085
+ text += " The limit is below the likely cost; it may stop early."
1078
1086
  if estimate.get("waiting"):
1079
1087
  text += f" {plural(estimate['waiting'], 'run')} waiting ahead."
1080
1088
  lines = [text]
@@ -1206,6 +1214,16 @@ def _estimate(plan: Plan) -> dict[str, Any] | None:
1206
1214
  return None
1207
1215
 
1208
1216
 
1217
+ def eval_tasks(plan: Plan) -> int:
1218
+ """Tasks in the final check: at most those held out from training."""
1219
+ usecase = plan.usecase or {}
1220
+ if usecase.get("split"):
1221
+ return int(usecase["split"].get("test") or 0)
1222
+ if usecase.get("scenarios"):
1223
+ return min(plan.check_tasks, withheld_tasks(int(usecase["scenarios"]), plan.share))
1224
+ return plan.check_tasks
1225
+
1226
+
1209
1227
  def _limits_panel(plan: Plan, budget: float, available: float | None, estimate: dict[str, Any] | None) -> None:
1210
1228
  """The console's "Limits for this run" box, on stderr (stdout is for the run itself)."""
1211
1229
  usecase = plan.usecase
@@ -1227,7 +1245,7 @@ def _limits_panel(plan: Plan, budget: float, available: float | None, estimate:
1227
1245
  ("Hyperparameters", "Advanced" if advanced else "Automatic"),
1228
1246
  ("Spending", f"max {ui.usd(budget, 0 if float(budget).is_integer() else 2)}"),
1229
1247
  ("Trains on", trains),
1230
- ("Eval set tasks", str(split.get("test") if split else plan.check_tasks)),
1248
+ ("Eval set tasks", str(eval_tasks(plan))),
1231
1249
  ]
1232
1250
  if plan.method:
1233
1251
  rows.append(("Training method", METHOD_LABEL.get(plan.method, plan.method)))
@@ -1350,7 +1368,7 @@ def start(
1350
1368
  least, most = run_limits()
1351
1369
  available, welcome = credit_available()
1352
1370
  estimate = _estimate(plan)
1353
- budget = limit if limit is not None else default_budget((estimate or {}).get("costUsd"), available, most, least)
1371
+ budget = limit if limit is not None else default_budget(likely_cost(estimate), available, most, least)
1354
1372
  problem = budget_problem(budget, least, most, available, welcome)
1355
1373
  if problem:
1356
1374
  raise fail(problem)
@@ -1546,7 +1564,7 @@ def estimate(
1546
1564
  def human() -> None:
1547
1565
  least, most = run_limits()
1548
1566
  available, _ = credit_available()
1549
- budget = default_budget(answer.get("costUsd"), available, most, least)
1567
+ budget = default_budget(likely_cost(answer), available, most, least)
1550
1568
  for line in estimate_lines(answer):
1551
1569
  ui.out.print(line, style="dim" if line.startswith(" ") else None)
1552
1570
  ui.out.print(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: corethink
3
- Version: 1.0.2
3
+ Version: 1.0.4
4
4
  Summary: The CoreThink Workbench from your terminal: environments, training, endpoints and more.
5
5
  Author-email: CoreThink <support@corethink.ai>
6
6
  License-Expression: MIT
@@ -5,7 +5,7 @@ import pytest
5
5
  ENVS = [
6
6
  {"id": "refunds-22982c", "org": "acme", "name": "Refunds"},
7
7
  {"id": "refunds-old-1a2b3c", "org": "acme", "name": "Refunds (old)"},
8
- {"id": "triage-77aa00", "org": "acme", "name": "Triage"},
8
+ {"id": "triage-77aa00", "org": "acme", "name": "Triage", "tools": ["lookup_order", "refund"]},
9
9
  ]
10
10
  STAGING_HASH = "b" * 64
11
11
  PROD_HASH = "a" * 64
@@ -143,6 +143,7 @@ def test_list(served, run):
143
143
  assert table.code == 0, table.err
144
144
  assert "v-aaaaaaaaaaaa" in table.out and "acme/refunds-22982c@production" in table.out
145
145
  assert "unavailable" in table.out
146
+ assert "Could not read 1 endpoint just now (unavailable)" in table.err
146
147
  assert "1 of 3 serving" in table.err and "https://gateway.test/v1" in table.err
147
148
  data = run("--json", "endpoint", "list").json()
148
149
  assert data["gateway"] == "https://gateway.test/v1"
@@ -416,6 +417,14 @@ def test_keys_usage(served, run):
416
417
  assert served.called("GET", "/api/v1/harnesses/keys/usage")[-1].params == {"environment": "triage-77aa00"}
417
418
  one = run("endpoint", "keys", "usage", "web-app")
418
419
  assert one.code == 0 and "2026-10-03" in one.out and "web-app, last 30 days: 6 requests" in one.err
420
+ # the table lists the days with requests (JSON keeps every day)
421
+ quiet = [{**day, "requests": 0, "tokens": 0, "cost": 0} for day in daily[:-1]] + daily[-1:]
422
+ served.on("GET", "/api/v1/harnesses/keys/usage", {"daily": quiet})
423
+ table = run("endpoint", "keys", "usage")
424
+ assert "2026-10-30" in table.out and "2026-10-29" not in table.out
425
+ assert len(run("--json", "endpoint", "keys", "usage").json()["daily"]) == 30
426
+ served.on("GET", "/api/v1/harnesses/keys/usage", {"daily": [{**day, "requests": 0} for day in quiet[:-1]]})
427
+ assert "No requests in the last 30 days." in run("endpoint", "keys", "usage").err
419
428
  served.on("GET", "/api/v1/harnesses/keys/usage", {"daily": None})
420
429
  assert "cannot be read" in run("endpoint", "keys", "usage").err
421
430
 
@@ -460,6 +469,20 @@ def test_call_with_the_env_key(served, run, monkeypatch):
460
469
  as_json = run("--json", "endpoint", "call", "refunds", "Hi", "--alias", "production").json()
461
470
  assert as_json["corethink_program"]["version"] == "harness-bbbbbbbbbbbb"
462
471
  assert served.called("POST", "/v1/chat/completions")[-1].body["model"] == "acme/refunds-22982c@production"
472
+ assert "works with tools" not in result.err
473
+
474
+
475
+ def test_call_warns_when_the_program_needs_tools_and_none_are_given(served, run, tmp_path, monkeypatch):
476
+ monkeypatch.setenv("CORETHINK_API_KEY", "ctk_endpointkey")
477
+ monkeypatch.setenv("CORETHINK_GATEWAY_URL", "https://gateway.test/v1")
478
+ served.on("POST", "/v1/chat/completions", ANSWER)
479
+ bare = run("endpoint", "call", "triage", "Refund order 7")
480
+ assert bare.code == 0, bare.err
481
+ assert "This program works with tools (lookup_order, refund)" in bare.err
482
+ assert "corethink env cat triage-77aa00 usecase.json" in bare.err
483
+ tools = tmp_path / "tools.json"
484
+ tools.write_text(json.dumps([{"type": "function", "function": {"name": "lookup_order"}}]))
485
+ assert "works with tools" not in run("endpoint", "call", "triage", "Hi", "--tools", str(tools)).err
463
486
 
464
487
 
465
488
  def test_call_a_program_directly_with_tools_from_usecase_json(wb, run, tmp_path, monkeypatch):
@@ -606,11 +629,17 @@ def test_snippets(served, run):
606
629
  "https://gateway.test/v1",
607
630
  )
608
631
  assert 'base_url="https://gateway.test/v1"' in python.out and 'model="acme/refunds-22982c@production"' in python.out
632
+ assert "works with tools" not in curl.err
609
633
  data = run("--json", "endpoint", "snippet", "acme/x@production").json()
610
634
  assert data["model"] == "acme/x@production" and set(data) == {
611
635
  "model",
612
636
  "base_url",
613
637
  "curl",
614
638
  "python",
639
+ "tools",
615
640
  }
641
+ assert data["tools"] == []
642
+ tooled = run("endpoint", "snippet", "triage")
643
+ assert "This program works with tools (lookup_order, refund). Send them as `tools`" in tooled.err
644
+ assert run("--json", "endpoint", "snippet", "triage").json()["tools"] == ["lookup_order", "refund"]
616
645
  assert run("endpoint", "snippet", "refunds", "--lang", "go").code == 2
@@ -250,6 +250,28 @@ def test_start_default_limit_follows_the_estimate(form, run):
250
250
  assert result.json()["job_id"] == RUN
251
251
 
252
252
 
253
+ def test_start_default_limit_follows_a_first_run_estimate_within_the_credit(form, run):
254
+ """No past run: the first-run estimate sets the limit (it was $20 whatever the estimate: MDM's Hub program cost $57)."""
255
+ first = {"typicalUsd": 12, "lowUsd": 8.4, "highUsd": 18, "drivers": [], "assumptions": []}
256
+ form.on(
257
+ "GET",
258
+ f"{JOBS}:estimate",
259
+ {"domain": "custom", "costUsd": None, "minutes": None, "basedOn": 0, "firstRun": first},
260
+ )
261
+ result = run("--json", "train", "start", "Payroll", "--yes")
262
+ assert result.code == 0, result.err
263
+ assert form.called("POST", JOBS)[-1].body["budgetUsd"] == 18
264
+ form.on(
265
+ "GET",
266
+ f"{JOBS}:estimate",
267
+ {"domain": "custom", "costUsd": None, "firstRun": {**first, "typicalUsd": 57.3, "lowUsd": 60}},
268
+ )
269
+ dear = run("train", "start", "Payroll", "--yes")
270
+ assert dear.code == 0, dear.err
271
+ assert form.called("POST", JOBS)[-1].body["budgetUsd"] == 50 # the credit available
272
+ assert "will likely stop early" in " ".join(dear.err.split())
273
+
274
+
253
275
  def test_start_advanced_settings_payload_with_providers_chosen(form, run):
254
276
  providers = {
255
277
  "qwen/qwen3.7-flash": {
@@ -957,3 +979,25 @@ def test_start_from_an_earlier_run_repeats_its_environment_and_settings(form, ru
957
979
 
958
980
  def test_start_needs_an_environment_or_a_run(form, run):
959
981
  assert run("train", "start", "--yes").code == 2
982
+
983
+
984
+ def test_a_first_runs_limit_below_its_likely_cost_is_flagged():
985
+ from corethink.commands.train import estimate_lines
986
+
987
+ first = {"typicalUsd": 57.3, "lowUsd": 40.1, "highUsd": 85.9, "drivers": [], "assumptions": []}
988
+ estimate = {"costUsd": None, "minutes": None, "firstRun": first}
989
+ assert "may stop early" not in estimate_lines(estimate, 86)[0]
990
+ assert estimate_lines(estimate, 45)[0].endswith("The limit is below the likely cost; it may stop early.")
991
+ assert estimate_lines(estimate, 30)[0].endswith("The limit is below the low end; it will likely stop early.")
992
+
993
+
994
+ def test_the_eval_set_is_at_most_the_tasks_held_out():
995
+ from corethink.commands.train import Plan, eval_tasks
996
+
997
+ made = Plan.__new__(Plan)
998
+ made.usecase, made.share, made.check_tasks = {"scenarios": 80}, 0.6, 40
999
+ assert eval_tasks(made) == 32 # 80 tasks, 48 to train on: 32 held out, not the 40 asked for
1000
+ made.usecase = {"scenarios": 200}
1001
+ assert eval_tasks(made) == 40
1002
+ made.usecase = {"scenarios": 200, "split": {"train": 150, "test": 50}}
1003
+ assert eval_tasks(made) == 50
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes