clixad 0.0.1-beta.25 → 0.0.1-beta.26

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/README.md CHANGED
@@ -15,6 +15,12 @@ cover it, for example `task ≈ 560–3.1k · free` or `task ≈ 7.5k–47k · n
15
15
  estimate from the size of the conversation and how tasks usually run; what is charged is what the
16
16
  model actually used.
17
17
 
18
+ `/effort` sets how long the model thinks before it answers, saved separately for each model.
19
+ Thinking is billed as output tokens like the answer, so a higher setting costs more credits per turn
20
+ and a lower one costs less. `auto` leaves the model at its own default, and a model with nothing to
21
+ adjust says so. The cost preview is not shown while a setting above the model's default is active,
22
+ because it is measured at the defaults.
23
+
18
24
  **Early beta.** Commands, interfaces and credit mechanics may change between beta versions. Run the
19
25
  most recent published build with:
20
26
 
package/dist/clixad.mjs CHANGED
@@ -1228,9 +1228,9 @@ var init_client = __esm({
1228
1228
  return retry;
1229
1229
  }
1230
1230
  /** Non-streaming chat. Kept for scripted/one-shot paths; the REPL streams. */
1231
- async chat(messages, model, tools, signal) {
1231
+ async chat(messages, model, tools, signal, effort) {
1232
1232
  const res = await this.postChat(
1233
- { model, messages, stream: false, ...tools ? { tools } : {} },
1233
+ { model, messages, stream: false, ...tools ? { tools } : {}, ...effort ? { reasoning_effort: effort } : {} },
1234
1234
  signal
1235
1235
  );
1236
1236
  if (res.status === 401) throw await authFailure(res, "chat");
@@ -1257,7 +1257,13 @@ var init_client = __esm({
1257
1257
  */
1258
1258
  async chatStream(messages, model, opts = {}) {
1259
1259
  const res = await this.postChat(
1260
- { model, messages, stream: true, ...opts.tools ? { tools: opts.tools } : {} },
1260
+ {
1261
+ model,
1262
+ messages,
1263
+ stream: true,
1264
+ ...opts.tools ? { tools: opts.tools } : {},
1265
+ ...opts.effort ? { reasoning_effort: opts.effort } : {}
1266
+ },
1261
1267
  opts.signal
1262
1268
  );
1263
1269
  if (res.status === 401) throw await authFailure(res, "chat");
@@ -1460,6 +1466,18 @@ function saveConfig(config) {
1460
1466
  } catch {
1461
1467
  }
1462
1468
  }
1469
+ function saveEffort(config, model, level) {
1470
+ const current = readStored();
1471
+ const onDisk = current.problem ? void 0 : current.stored.effort;
1472
+ const next = {};
1473
+ for (const [id, value] of Object.entries(onDisk && typeof onDisk === "object" ? onDisk : {})) {
1474
+ if (typeof value === "string") next[id] = value;
1475
+ }
1476
+ if (level === void 0) delete next[model];
1477
+ else next[model] = level;
1478
+ config.effort = Object.keys(next).length ? next : void 0;
1479
+ saveConfig(config);
1480
+ }
1463
1481
  function configPath() {
1464
1482
  return CONFIG_PATH;
1465
1483
  }
@@ -4173,6 +4191,9 @@ async function runAgent(client, task, opts) {
4173
4191
  onProgress?.(`${description}: starting`);
4174
4192
  const sub = await runAgent(client, prompt, {
4175
4193
  model: opts.model,
4194
+ // The setting somebody chose for this model, the same as the parent's
4195
+ // calls: a sub-agent's credits land on the same turn.
4196
+ effort: opts.effort,
4176
4197
  root: opts.root,
4177
4198
  // A sub-agent that was told it can search the web, on a gateway that
4178
4199
  // cannot, spends its whole step budget discovering that.
@@ -4226,6 +4247,7 @@ ${NO_SEARCH_INSTRUCTION}`,
4226
4247
  let turn;
4227
4248
  try {
4228
4249
  turn = await client.chatStream(messages, opts.model, {
4250
+ effort: opts.effort,
4229
4251
  tools: opts.tools ? opts.tools() : TOOL_SCHEMA,
4230
4252
  signal: opts.signal,
4231
4253
  onDelta: (text) => emit({ type: "delta", text })
@@ -5097,6 +5119,50 @@ var init_version = __esm({
5097
5119
  }
5098
5120
  });
5099
5121
 
5122
+ // src/effort.ts
5123
+ function isLevel(value) {
5124
+ return typeof value === "string" && EFFORT_LEVELS.includes(value);
5125
+ }
5126
+ function effortsOf(info) {
5127
+ return (info?.efforts ?? []).filter(isLevel);
5128
+ }
5129
+ function parseEffortArg(arg) {
5130
+ const v = arg.trim().toLowerCase();
5131
+ if (v === "auto" || v === "default") return "auto";
5132
+ if (v === "none") return "off";
5133
+ return isLevel(v) ? v : void 0;
5134
+ }
5135
+ function resolveEffort(info, requested) {
5136
+ if (!isLevel(requested)) return void 0;
5137
+ const efforts = effortsOf(info);
5138
+ if (efforts.length === 0) return void 0;
5139
+ if (!isLevel(info?.default_effort)) return requested === "off" && efforts.includes("off") ? "off" : void 0;
5140
+ if (efforts.includes(requested)) return requested;
5141
+ const below = efforts.filter((e) => rank(e) <= rank(requested));
5142
+ return below.length ? below[below.length - 1] : efforts[0];
5143
+ }
5144
+ function savedEffort(config, model) {
5145
+ const value = config.effort?.[model];
5146
+ return typeof value === "string" ? value : void 0;
5147
+ }
5148
+ function effortRaised(info, level) {
5149
+ if (!level || !isLevel(info?.default_effort)) return false;
5150
+ return rank(level) > rank(info.default_effort);
5151
+ }
5152
+ function effortLabel(level) {
5153
+ if (level === "off") return "thinking off";
5154
+ if (level === "on") return "thinking on";
5155
+ return `${level} effort`;
5156
+ }
5157
+ var EFFORT_LEVELS, rank;
5158
+ var init_effort = __esm({
5159
+ "src/effort.ts"() {
5160
+ "use strict";
5161
+ EFFORT_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
5162
+ rank = (level) => EFFORT_LEVELS.indexOf(level);
5163
+ }
5164
+ });
5165
+
5100
5166
  // src/mentions.ts
5101
5167
  function findMentions(text) {
5102
5168
  const out = [];
@@ -5187,7 +5253,9 @@ async function compact(client, model, messages, opts = {}) {
5187
5253
  const turn = await client.chatStream(
5188
5254
  [...messages.slice(0, cut), { role: "user", content: SUMMARY_PROMPT }],
5189
5255
  model,
5190
- { signal: opts.signal }
5256
+ // The `/effort` setting applies to every call on the model, a summary
5257
+ // included: somebody who turned it down to save credits saves them here too.
5258
+ { signal: opts.signal, effort: opts.effort }
5191
5259
  );
5192
5260
  const summary = messageText(turn.message).trim() || "(no summary returned)";
5193
5261
  const compacted = [
@@ -5460,6 +5528,10 @@ var init_commands = __esm({
5460
5528
  { name: "help", desc: "list the commands" },
5461
5529
  { name: "model", desc: "switch model" },
5462
5530
  { name: "models", desc: "list models + prices" },
5531
+ // Deliberately *no* `args`, for `/model`'s reason: an arg hint makes enter
5532
+ // complete the command instead of running it, and the picker is the point.
5533
+ // `/effort low` still works when typed in full.
5534
+ { name: "effort", desc: "how long the model thinks (more thinking, more credits)" },
5463
5535
  { name: "mode", args: "[name]", desc: "permission mode (or press shift+tab)" },
5464
5536
  { name: "wallet", desc: "balance & ads today" },
5465
5537
  // Not "watch an ad": nothing on the wall is a video. CPX serves sign-up
@@ -6024,6 +6096,7 @@ function App({
6024
6096
  const [spent, setSpent] = useState(0);
6025
6097
  const [freeTier, setFreeTier] = useState(wallet?.free_tier);
6026
6098
  const [model, setModel2] = useState(config.model);
6099
+ const [effortBy, setEffortBy] = useState(() => config.effort ?? {});
6027
6100
  const modelRef = useRef(config.model);
6028
6101
  const [mode, setMode] = useState("normal");
6029
6102
  const switchModel = useCallback((id) => {
@@ -6513,7 +6586,7 @@ function App({
6513
6586
  const res = await compact(client, model2, [
6514
6587
  { role: "system", content: contextRef.current.systemPrompt },
6515
6588
  ...history
6516
- ], { signal: ac.signal });
6589
+ ], { signal: ac.signal, effort: savedEffort(config, model2) });
6517
6590
  if (res) {
6518
6591
  history = res.messages.slice(1);
6519
6592
  setMessages(history);
@@ -6527,6 +6600,10 @@ function App({
6527
6600
  }
6528
6601
  const result = await runAgent(client, task, {
6529
6602
  model: model2,
6603
+ // Read now rather than from the render, for `modelRef`'s reason: a
6604
+ // setting chosen in the same tick as this turn starts is in `config`
6605
+ // already and not yet in any state.
6606
+ effort: savedEffort(config, model2),
6530
6607
  root,
6531
6608
  history,
6532
6609
  attachments: images,
@@ -6996,7 +7073,7 @@ ${NO_SEARCH_INSTRUCTION}`),
6996
7073
  client,
6997
7074
  model,
6998
7075
  [{ role: "system", content: contextRef.current.systemPrompt }, ...messagesRef.current],
6999
- { signal }
7076
+ { signal, effort: savedEffort(config, model) }
7000
7077
  );
7001
7078
  if (!res) return void push({ kind: "notice", text: " nothing to compact yet" });
7002
7079
  setMessages(res.messages.slice(1));
@@ -7048,6 +7125,64 @@ ${NO_SEARCH_INSTRUCTION}`),
7048
7125
  });
7049
7126
  });
7050
7127
  return;
7128
+ // How long the model thinks, saved per model. The picker offers only
7129
+ // what the current model takes, so a stored setting is always one it
7130
+ // has; a typed level it does not have moves to the nearest one below,
7131
+ // the gateway's rule, and says so. Every figure about cost stays in
7132
+ // words: what a setting costs depends on the task, and the meta line
7133
+ // under each turn already shows what it did cost.
7134
+ case "effort":
7135
+ await runBusy(LOADING, async (signal) => {
7136
+ const models = catalogRef.current.length ? catalogRef.current : await client.models(signal);
7137
+ catalogRef.current = models;
7138
+ const info2 = models.find((m) => m.id === model);
7139
+ const name = info2?.label ?? model;
7140
+ const levels = effortsOf(info2);
7141
+ const fallback = info2?.default_effort;
7142
+ if (!info2 || levels.length === 0) {
7143
+ const why = info2?.efforts === void 0 ? "this gateway does not offer effort settings" : fallback === "on" ? `${name} always thinks before it answers, and has no setting for how long` : `${name} has no effort setting`;
7144
+ return void push({ kind: "notice", text: ` ${why}` });
7145
+ }
7146
+ const defaultWords = fallback ? effortLabel(fallback) : "its default";
7147
+ const apply2 = (choice, note = "") => {
7148
+ saveEffort(config, model, choice);
7149
+ setEffortBy({ ...config.effort ?? {} });
7150
+ const now = choice ? effortLabel(choice) : `auto (${defaultWords})`;
7151
+ push({ kind: "notice", text: ` ${name}: ${now}${note}` });
7152
+ };
7153
+ if (arg) {
7154
+ const wanted = parseEffortArg(arg);
7155
+ if (!wanted) {
7156
+ return void push({
7157
+ kind: "notice",
7158
+ tone: "warn",
7159
+ text: ` unknown effort: ${arg}. ${name} takes ${["auto", ...levels].join(", ")}`
7160
+ });
7161
+ }
7162
+ if (wanted === "auto") return apply2(void 0);
7163
+ const resolved = resolveEffort(info2, wanted);
7164
+ if (!resolved) return apply2(void 0, `. ${name} can only switch its thinking off`);
7165
+ return apply2(resolved, resolved === wanted ? "" : `. It has no ${wanted}, so this is the nearest below`);
7166
+ }
7167
+ const current = resolveEffort(info2, savedEffort(config, model));
7168
+ const items = [
7169
+ { value: "auto", label: "auto", hint: `the model's default (${defaultWords})`, current: !current },
7170
+ ...levels.map((level) => ({
7171
+ value: level,
7172
+ label: level,
7173
+ hint: level === fallback ? "same as auto" : effortRaised(info2, level) ? "more thinking, more credits" : "less thinking, fewer credits",
7174
+ current: current === level
7175
+ }))
7176
+ ];
7177
+ setPickerSel(Math.max(0, items.findIndex((i) => i.current)));
7178
+ setPicker({
7179
+ title: `Effort for ${name}`,
7180
+ subtitle: "Saved for this model. Thinking is billed as output, like the answer.",
7181
+ items,
7182
+ onPick: (choice) => apply2(choice.value === "auto" ? void 0 : choice.value)
7183
+ });
7184
+ });
7185
+ return;
7051
7186
  case "models":
7052
7187
  await runBusy(LOADING, async (signal) => {
7053
7188
  const ms = await client.models(signal);
@@ -7722,8 +7857,9 @@ ${dropped.map((l) => ` \xB7 ${l}`).join("\n")}`
7722
7857
  const typed = toText(editor);
7723
7858
  let estimate;
7724
7859
  const info = catalogRef.current.find((m) => m.id === model);
7860
+ const effortNow = info ? resolveEffort(info, effortBy[model]) : effortBy[model];
7725
7861
  const shape = client.taskShape;
7726
- if (!busy && info && shape && balance !== void 0 && typed.trim() !== "" && !/^[/!]/.test(typed.trimStart())) {
7862
+ if (!busy && info && shape && balance !== void 0 && !effortRaised(info, resolveEffort(info, effortBy[model])) && typed.trim() !== "" && !/^[/!]/.test(typed.trimStart())) {
7727
7863
  const range = estimateTask(info, conversationUsed + schemaTokens + estimateTokens(typed), shape);
7728
7864
  const onFree = Boolean(info.free && freeTier?.models.includes(model));
7729
7865
  const verdict = taskVerdict(range, { balance, ...onFree ? { freeLeft: freeTier.credits_left } : {} });
@@ -7734,6 +7870,7 @@ ${dropped.map((l) => ` \xB7 ${l}`).join("\n")}`
7734
7870
  ` ${statusLine(
7735
7871
  {
7736
7872
  model,
7873
+ ...effortNow ? { effort: effortNow } : {},
7737
7874
  balance,
7738
7875
  spent,
7739
7876
  contextPct,
@@ -7932,6 +8069,8 @@ function statusRow(o, estimate, cols) {
7932
8069
  const burn = o.spent > 0 && o.minutes >= 1 ? `${Math.round(o.spent / o.minutes).toLocaleString("en-US")} cr/min` : void 0;
7933
8070
  const clauses = [
7934
8071
  { text: o.model, rank: KEEP_ALWAYS },
8072
+ // Beside the model it qualifies, and given up with the free allowance.
8073
+ { text: o.effort === void 0 ? void 0 : effortLabel(o.effort), rank: 4 },
7935
8074
  {
7936
8075
  // "? cr" rather than "0 cr": unknown is not empty, and an empty-looking
7937
8076
  // wallet is the one misreading on this row that frightens somebody.
@@ -8017,6 +8156,7 @@ var init_app = __esm({
8017
8156
  init_todos();
8018
8157
  init_shells();
8019
8158
  init_config();
8159
+ init_effort();
8020
8160
  init_login();
8021
8161
  init_banner();
8022
8162
  init_agent();
@@ -8165,6 +8305,7 @@ function clearTerminalTitle(out = titleStream()) {
8165
8305
 
8166
8306
  // src/main.ts
8167
8307
  init_version();
8308
+ init_effort();
8168
8309
  var c = palette({ isTTY: process.stdout.isTTY, env: process.env });
8169
8310
  async function main() {
8170
8311
  setTerminalTitle();
@@ -8461,6 +8602,7 @@ async function printCmd(client, config, task) {
8461
8602
  try {
8462
8603
  const res = await runAgent(client, task, {
8463
8604
  model: config.model,
8605
+ effort: savedEffort(config, config.model),
8464
8606
  root: process.cwd(),
8465
8607
  systemPrompt: collectContext(process.cwd()).systemPrompt + (capabilities?.web_search ? "" : `
8466
8608
 
@@ -8606,6 +8748,7 @@ async function runTurn(client, config, messages) {
8606
8748
  let started = false;
8607
8749
  try {
8608
8750
  const result = await client.chatStream(messages, config.model, {
8751
+ effort: savedEffort(config, config.model),
8609
8752
  onDelta: (chunk) => {
8610
8753
  if (!started) process.stdout.write(c.green("assistant "));
8611
8754
  started = true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clixad",
3
- "version": "0.0.1-beta.25",
3
+ "version": "0.0.1-beta.26",
4
4
  "description": "Free AI coding agent in your terminal, funded by rewarded ads.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",