clixad 0.0.1-beta.24 → 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
@@ -94,14 +94,62 @@ var init_estimate = __esm({
94
94
  });
95
95
 
96
96
  // src/sponsor/formats.ts
97
- var EXTRA_AD_FORMATS;
97
+ function isAdFormat(value) {
98
+ return typeof value === "string" && AD_FORMATS.includes(value);
99
+ }
100
+ var AD_FORMATS, EXTRA_AD_FORMATS;
98
101
  var init_formats = __esm({
99
102
  "src/sponsor/formats.ts"() {
100
103
  "use strict";
104
+ AD_FORMATS = ["top_bar", "sidebar_card", "status_bar"];
101
105
  EXTRA_AD_FORMATS = ["sidebar_card", "status_bar"];
102
106
  }
103
107
  });
104
108
 
109
+ // src/sponsor/direct.ts
110
+ function sanitizeAdText(raw) {
111
+ return raw.replace(/\u001b\[[0-?]*[ -/]*[@-~]/g, "").replace(/\u001b\][^\u0007\u001b]*(?:\u0007|\u001b\\)?/g, "").replace(/[\u0000-\u001f\u007f-\u009f]/g, " ").replace(/[‪-‮⁦-⁩‎‏؜]/g, "").replace(/[​-‍⁠­]/g, "").replace(/\s+/g, " ").trim();
112
+ }
113
+ function parseDirectLines(body) {
114
+ const raw = body?.lines;
115
+ if (!Array.isArray(raw)) return [];
116
+ const out = [];
117
+ const seen = /* @__PURE__ */ new Set();
118
+ for (const item of raw) {
119
+ if (!item || typeof item !== "object") continue;
120
+ const e = item;
121
+ if (!isAdFormat(e.format) || seen.has(e.format)) continue;
122
+ if (e.hold === true) {
123
+ out.push({ format: e.format, hold: true });
124
+ seen.add(e.format);
125
+ continue;
126
+ }
127
+ if (typeof e.text !== "string" || typeof e.serve_token !== "string" || typeof e.click_url !== "string") continue;
128
+ const text = sanitizeAdText(e.text).slice(0, MAX_TEXT);
129
+ if (!text || !e.serve_token || e.serve_token.length > 2e3) continue;
130
+ let clickUrl;
131
+ try {
132
+ const url = new URL(e.click_url);
133
+ if (url.protocol !== "https:" && url.protocol !== "http:") continue;
134
+ clickUrl = url.toString();
135
+ } catch {
136
+ continue;
137
+ }
138
+ out.push({ format: e.format, text, serveToken: e.serve_token, clickUrl });
139
+ seen.add(e.format);
140
+ }
141
+ return out;
142
+ }
143
+ var DIRECT_ASK_TIMEOUT_MS, MAX_TEXT;
144
+ var init_direct = __esm({
145
+ "src/sponsor/direct.ts"() {
146
+ "use strict";
147
+ init_formats();
148
+ DIRECT_ASK_TIMEOUT_MS = 1500;
149
+ MAX_TEXT = 200;
150
+ }
151
+ });
152
+
105
153
  // src/edge.ts
106
154
  function detectEdgeBlock(status, headers, body) {
107
155
  if (status !== 403) return void 0;
@@ -498,6 +546,7 @@ var init_client = __esm({
498
546
  "use strict";
499
547
  init_estimate();
500
548
  init_formats();
549
+ init_direct();
501
550
  init_edge();
502
551
  CHAT_ENCODED_CONTENT_TYPE = "application/vnd.clixad.chat+gzip";
503
552
  LEDGER_REASONS = {
@@ -645,6 +694,7 @@ var init_client = __esm({
645
694
  return { status: "closed", message: closed.message, waitlistUrl: closed.waitlist_url };
646
695
  }
647
696
  if (!res.ok && !("status" in data && data.status)) {
697
+ if (res.status >= 500 || res.status === 429) return { status: "unavailable", error: `http ${res.status}` };
648
698
  return { status: "error", error: `http ${res.status}` };
649
699
  }
650
700
  return data;
@@ -981,6 +1031,51 @@ var init_client = __esm({
981
1031
  return 0;
982
1032
  }
983
1033
  }
1034
+ /**
1035
+ * Ask for booked lines (step 2): one ask per model call, naming the slots
1036
+ * that fit the terminal now. See `sponsor/direct.ts` for the contract.
1037
+ *
1038
+ * Never throws, for `sponsorConfig`'s reason. A 404 means a gateway that
1039
+ * does not know the route, so `supported: false` tells the caller to stop
1040
+ * asking for the rest of the session; every other failure (a timeout, a 429,
1041
+ * a 5xx, a malformed body) is simply "nothing booked" this time.
1042
+ */
1043
+ async directSponsor(formats, cols, signal) {
1044
+ try {
1045
+ const res = await gatewayFetch(`${this.config.gatewayUrl}/v1/sponsor/direct`, {
1046
+ method: "POST",
1047
+ headers: this.headers(),
1048
+ ...signal ? { signal } : {},
1049
+ body: JSON.stringify({ formats, cols })
1050
+ });
1051
+ if (res.status === 404) return { supported: false, lines: [] };
1052
+ if (!res.ok) return { supported: true, lines: [] };
1053
+ return { supported: true, lines: parseDirectLines(await res.json()) };
1054
+ } catch {
1055
+ return { supported: true, lines: [] };
1056
+ }
1057
+ }
1058
+ /**
1059
+ * Report the booked lines a turn drew, in one call, after the turn is over.
1060
+ * Never to `/v1/sponsor/impression`, which would credit them as ADtention's.
1061
+ * Never throws; answers what was credited, for the tally and for tests.
1062
+ */
1063
+ async reportDirectImpressions(impressions, signal) {
1064
+ if (impressions.length === 0) return 0;
1065
+ try {
1066
+ const res = await gatewayFetch(`${this.config.gatewayUrl}/v1/sponsor/direct/impression`, {
1067
+ method: "POST",
1068
+ headers: this.headers(),
1069
+ ...signal ? { signal } : {},
1070
+ body: JSON.stringify({ impressions: impressions.map((i) => ({ format: i.format, serve_token: i.serveToken })) })
1071
+ });
1072
+ if (!res.ok) return 0;
1073
+ const body = await res.json();
1074
+ return typeof body.credited === "number" ? body.credited : 0;
1075
+ } catch {
1076
+ return 0;
1077
+ }
1078
+ }
984
1079
  /**
985
1080
  * Search the web, through the gateway.
986
1081
  *
@@ -1133,9 +1228,9 @@ var init_client = __esm({
1133
1228
  return retry;
1134
1229
  }
1135
1230
  /** Non-streaming chat. Kept for scripted/one-shot paths; the REPL streams. */
1136
- async chat(messages, model, tools, signal) {
1231
+ async chat(messages, model, tools, signal, effort) {
1137
1232
  const res = await this.postChat(
1138
- { model, messages, stream: false, ...tools ? { tools } : {} },
1233
+ { model, messages, stream: false, ...tools ? { tools } : {}, ...effort ? { reasoning_effort: effort } : {} },
1139
1234
  signal
1140
1235
  );
1141
1236
  if (res.status === 401) throw await authFailure(res, "chat");
@@ -1162,7 +1257,13 @@ var init_client = __esm({
1162
1257
  */
1163
1258
  async chatStream(messages, model, opts = {}) {
1164
1259
  const res = await this.postChat(
1165
- { 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
+ },
1166
1267
  opts.signal
1167
1268
  );
1168
1269
  if (res.status === 401) throw await authFailure(res, "chat");
@@ -1365,6 +1466,18 @@ function saveConfig(config) {
1365
1466
  } catch {
1366
1467
  }
1367
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
+ }
1368
1481
  function configPath() {
1369
1482
  return CONFIG_PATH;
1370
1483
  }
@@ -1443,11 +1556,24 @@ async function githubLogin(client, config, start, invite, report, signal) {
1443
1556
  openBrowser(start.verification_uri);
1444
1557
  const deadline = Date.now() + start.expires_in * 1e3;
1445
1558
  let interval = Math.max(start.interval, 1);
1559
+ let unavailableSince;
1446
1560
  while (Date.now() < deadline) {
1447
1561
  await sleep2(interval * 1e3, signal);
1448
1562
  if (signal?.aborted) return { status: "aborted" };
1449
1563
  report.tick?.();
1450
- const poll = await client.devicePoll(start.session, invite);
1564
+ let poll;
1565
+ try {
1566
+ poll = await client.devicePoll(start.session, invite);
1567
+ } catch (err) {
1568
+ if (!(err instanceof GatewayUnreachableError)) throw err;
1569
+ poll = { status: "unavailable", error: err.message };
1570
+ }
1571
+ if (poll.status === "unavailable") {
1572
+ unavailableSince ??= Date.now();
1573
+ if (Date.now() - unavailableSince > UNAVAILABLE_GIVE_UP_MS) return { status: "failed", error: poll.error };
1574
+ continue;
1575
+ }
1576
+ unavailableSince = void 0;
1451
1577
  if (poll.status === "pending") {
1452
1578
  if (poll.interval) interval = poll.interval;
1453
1579
  continue;
@@ -1475,10 +1601,19 @@ async function githubLogin(client, config, start, invite, report, signal) {
1475
1601
  }
1476
1602
  };
1477
1603
  }
1478
- return { status: "failed", error: poll.error ?? poll.status };
1604
+ return { status: "failed", error: describePollError(poll.error ?? poll.status) };
1479
1605
  }
1480
1606
  return { status: "timeout" };
1481
1607
  }
1608
+ function describePollError(error) {
1609
+ if (error === "unknown_session") {
1610
+ return "the server restarted while you were signing in, so this code no longer works. Sign in again.";
1611
+ }
1612
+ if (error === "login_unavailable") {
1613
+ return "GitHub approved the sign-in, but the server could not finish it. Try again in a minute.";
1614
+ }
1615
+ return error;
1616
+ }
1482
1617
  async function devLogin(client, config, email, invite) {
1483
1618
  let res;
1484
1619
  try {
@@ -1517,7 +1652,7 @@ function sleep2(ms, signal) {
1517
1652
  signal.addEventListener("abort", done, { once: true });
1518
1653
  });
1519
1654
  }
1520
- var TERMS_URL, PRIVACY_URL, TERMS_NOTICE;
1655
+ var TERMS_URL, PRIVACY_URL, TERMS_NOTICE, UNAVAILABLE_GIVE_UP_MS;
1521
1656
  var init_login = __esm({
1522
1657
  "src/login.ts"() {
1523
1658
  "use strict";
@@ -1529,6 +1664,7 @@ var init_login = __esm({
1529
1664
  TERMS_NOTICE = `By creating an account you confirm that you are at least 18 and that you agree to
1530
1665
  the Terms \u2014 ${TERMS_URL}
1531
1666
  and the Privacy Policy \u2014 ${PRIVACY_URL}`;
1667
+ UNAVAILABLE_GIVE_UP_MS = 18e4;
1532
1668
  }
1533
1669
  });
1534
1670
 
@@ -4055,6 +4191,9 @@ async function runAgent(client, task, opts) {
4055
4191
  onProgress?.(`${description}: starting`);
4056
4192
  const sub = await runAgent(client, prompt, {
4057
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,
4058
4197
  root: opts.root,
4059
4198
  // A sub-agent that was told it can search the web, on a gateway that
4060
4199
  // cannot, spends its whole step budget discovering that.
@@ -4108,6 +4247,7 @@ ${NO_SEARCH_INSTRUCTION}`,
4108
4247
  let turn;
4109
4248
  try {
4110
4249
  turn = await client.chatStream(messages, opts.model, {
4250
+ effort: opts.effort,
4111
4251
  tools: opts.tools ? opts.tools() : TOOL_SCHEMA,
4112
4252
  signal: opts.signal,
4113
4253
  onDelta: (text) => emit({ type: "delta", text })
@@ -4979,6 +5119,50 @@ var init_version = __esm({
4979
5119
  }
4980
5120
  });
4981
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
+
4982
5166
  // src/mentions.ts
4983
5167
  function findMentions(text) {
4984
5168
  const out = [];
@@ -5069,7 +5253,9 @@ async function compact(client, model, messages, opts = {}) {
5069
5253
  const turn = await client.chatStream(
5070
5254
  [...messages.slice(0, cut), { role: "user", content: SUMMARY_PROMPT }],
5071
5255
  model,
5072
- { 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 }
5073
5259
  );
5074
5260
  const summary = messageText(turn.message).trim() || "(no summary returned)";
5075
5261
  const compacted = [
@@ -5342,6 +5528,10 @@ var init_commands = __esm({
5342
5528
  { name: "help", desc: "list the commands" },
5343
5529
  { name: "model", desc: "switch model" },
5344
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)" },
5345
5535
  { name: "mode", args: "[name]", desc: "permission mode (or press shift+tab)" },
5346
5536
  { name: "wallet", desc: "balance & ads today" },
5347
5537
  // Not "watch an ad": nothing on the wall is a video. CPX serves sign-up
@@ -5865,6 +6055,7 @@ function App({
5865
6055
  notices: notices2,
5866
6056
  sponsorServe,
5867
6057
  sponsorFormats,
6058
+ sponsorDirect,
5868
6059
  initHint
5869
6060
  }) {
5870
6061
  const { exit } = useApp();
@@ -5905,6 +6096,7 @@ function App({
5905
6096
  const [spent, setSpent] = useState(0);
5906
6097
  const [freeTier, setFreeTier] = useState(wallet?.free_tier);
5907
6098
  const [model, setModel2] = useState(config.model);
6099
+ const [effortBy, setEffortBy] = useState(() => config.effort ?? {});
5908
6100
  const modelRef = useRef(config.model);
5909
6101
  const [mode, setMode] = useState("normal");
5910
6102
  const switchModel = useCallback((id) => {
@@ -5973,6 +6165,8 @@ function App({
5973
6165
  const sponsorPrevRef = useRef(void 0);
5974
6166
  const sponsorCallRef = useRef(0);
5975
6167
  const pendingImpressionRef = useRef(/* @__PURE__ */ new Map());
6168
+ const pendingDirectRef = useRef(/* @__PURE__ */ new Map());
6169
+ const directSupportedRef = useRef(true);
5976
6170
  const drawnRef = useRef(/* @__PURE__ */ new Set());
5977
6171
  const sponsorSlotRef = useRef(
5978
6172
  /* @__PURE__ */ new Map()
@@ -6184,8 +6378,7 @@ function App({
6184
6378
  } else {
6185
6379
  setSponsor(null);
6186
6380
  }
6187
- for (const format of ["top_bar", ...EXTRA_AD_FORMATS]) {
6188
- if (!adFitsFormat(format, colsRef.current)) continue;
6381
+ const fetchNetwork = (format) => {
6189
6382
  void remoteSponsorRef.current[format].fetch().then((line2) => {
6190
6383
  if (!line2 || call !== sponsorCallRef.current) return;
6191
6384
  if (!adFitsFormat(format, colsRef.current)) return;
@@ -6203,15 +6396,55 @@ function App({
6203
6396
  recordImpression(tallyRef.current, line2.id);
6204
6397
  pendingImpressionRef.current.set(format, { call, impression: line2.impression });
6205
6398
  });
6206
- }
6399
+ };
6400
+ const paintDirect = (line2) => {
6401
+ const { format } = line2;
6402
+ if (call !== sponsorCallRef.current) return;
6403
+ if (!adFitsFormat(format, colsRef.current)) return;
6404
+ if (format !== "top_bar" && modalOpenRef.current) return;
6405
+ const drawnId = `direct:${call}:${format}`;
6406
+ if (format === "top_bar") {
6407
+ if (!sponsorRow(line2.text, colsRef.current, "network")) return;
6408
+ setSponsor({ text: line2.text, origin: "network", impressionId: drawnId });
6409
+ } else {
6410
+ setAdSlots((slots) => ({ ...slots, [format]: { text: line2.text, impressionId: drawnId } }));
6411
+ }
6412
+ pendingDirectRef.current.set(format, { call, serveToken: line2.serveToken, drawnId });
6413
+ };
6414
+ const fitting = AD_FORMATS.filter((format) => adFitsFormat(format, colsRef.current));
6415
+ void (async () => {
6416
+ let direct = [];
6417
+ if (directSupportedRef.current && fitting.length > 0) {
6418
+ const ask3 = sponsorDirect ?? ((formats, cols2, signal) => client.directSponsor(formats, cols2, signal));
6419
+ let answer = { supported: true, lines: [] };
6420
+ try {
6421
+ answer = await ask3(fitting, colsRef.current, AbortSignal.timeout(DIRECT_ASK_TIMEOUT_MS));
6422
+ } catch {
6423
+ }
6424
+ if (!answer.supported) directSupportedRef.current = false;
6425
+ direct = answer.lines;
6426
+ }
6427
+ if (call !== sponsorCallRef.current) return;
6428
+ for (const format of ["top_bar", ...EXTRA_AD_FORMATS]) {
6429
+ if (!adFitsFormat(format, colsRef.current)) continue;
6430
+ const booked = direct.find((line2) => line2.format === format);
6431
+ if (booked && "hold" in booked) continue;
6432
+ if (booked) paintDirect(booked);
6433
+ else fetchNetwork(format);
6434
+ }
6435
+ })();
6207
6436
  },
6208
- [config.sponsor, stdout, wallet]
6437
+ [config.sponsor, stdout, wallet, client, sponsorDirect]
6209
6438
  );
6210
6439
  const reportSponsor = useCallback(() => {
6211
6440
  const pending = [...pendingImpressionRef.current.values()];
6441
+ const pendingDirect = [...pendingDirectRef.current.entries()];
6212
6442
  const drawn = drawnRef.current;
6213
6443
  pendingImpressionRef.current = /* @__PURE__ */ new Map();
6444
+ pendingDirectRef.current = /* @__PURE__ */ new Map();
6214
6445
  drawnRef.current = /* @__PURE__ */ new Set();
6446
+ const directReports = pendingDirect.filter(([, p]) => p.call === sponsorCallRef.current && drawn.has(p.drawnId)).map(([format, p]) => ({ format, serveToken: p.serveToken }));
6447
+ if (directReports.length > 0) void client.reportDirectImpressions(directReports);
6215
6448
  const reports = pending.filter((p) => p.call === sponsorCallRef.current).filter((p) => drawn.has(p.impression.impressionId)).map((p) => p.impression);
6216
6449
  if (reports.length === 0) return;
6217
6450
  void (async () => {
@@ -6353,7 +6586,7 @@ function App({
6353
6586
  const res = await compact(client, model2, [
6354
6587
  { role: "system", content: contextRef.current.systemPrompt },
6355
6588
  ...history
6356
- ], { signal: ac.signal });
6589
+ ], { signal: ac.signal, effort: savedEffort(config, model2) });
6357
6590
  if (res) {
6358
6591
  history = res.messages.slice(1);
6359
6592
  setMessages(history);
@@ -6367,6 +6600,10 @@ function App({
6367
6600
  }
6368
6601
  const result = await runAgent(client, task, {
6369
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),
6370
6607
  root,
6371
6608
  history,
6372
6609
  attachments: images,
@@ -6836,7 +7073,7 @@ ${NO_SEARCH_INSTRUCTION}`),
6836
7073
  client,
6837
7074
  model,
6838
7075
  [{ role: "system", content: contextRef.current.systemPrompt }, ...messagesRef.current],
6839
- { signal }
7076
+ { signal, effort: savedEffort(config, model) }
6840
7077
  );
6841
7078
  if (!res) return void push({ kind: "notice", text: " nothing to compact yet" });
6842
7079
  setMessages(res.messages.slice(1));
@@ -6888,6 +7125,64 @@ ${NO_SEARCH_INSTRUCTION}`),
6888
7125
  });
6889
7126
  });
6890
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;
6891
7186
  case "models":
6892
7187
  await runBusy(LOADING, async (signal) => {
6893
7188
  const ms = await client.models(signal);
@@ -7562,8 +7857,9 @@ ${dropped.map((l) => ` \xB7 ${l}`).join("\n")}`
7562
7857
  const typed = toText(editor);
7563
7858
  let estimate;
7564
7859
  const info = catalogRef.current.find((m) => m.id === model);
7860
+ const effortNow = info ? resolveEffort(info, effortBy[model]) : effortBy[model];
7565
7861
  const shape = client.taskShape;
7566
- 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())) {
7567
7863
  const range = estimateTask(info, conversationUsed + schemaTokens + estimateTokens(typed), shape);
7568
7864
  const onFree = Boolean(info.free && freeTier?.models.includes(model));
7569
7865
  const verdict = taskVerdict(range, { balance, ...onFree ? { freeLeft: freeTier.credits_left } : {} });
@@ -7574,6 +7870,7 @@ ${dropped.map((l) => ` \xB7 ${l}`).join("\n")}`
7574
7870
  ` ${statusLine(
7575
7871
  {
7576
7872
  model,
7873
+ ...effortNow ? { effort: effortNow } : {},
7577
7874
  balance,
7578
7875
  spent,
7579
7876
  contextPct,
@@ -7772,6 +8069,8 @@ function statusRow(o, estimate, cols) {
7772
8069
  const burn = o.spent > 0 && o.minutes >= 1 ? `${Math.round(o.spent / o.minutes).toLocaleString("en-US")} cr/min` : void 0;
7773
8070
  const clauses = [
7774
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 },
7775
8074
  {
7776
8075
  // "? cr" rather than "0 cr": unknown is not empty, and an empty-looking
7777
8076
  // wallet is the one misreading on this row that frightens somebody.
@@ -7857,6 +8156,7 @@ var init_app = __esm({
7857
8156
  init_todos();
7858
8157
  init_shells();
7859
8158
  init_config();
8159
+ init_effort();
7860
8160
  init_login();
7861
8161
  init_banner();
7862
8162
  init_agent();
@@ -7876,6 +8176,7 @@ var init_app = __esm({
7876
8176
  init_provider();
7877
8177
  init_network();
7878
8178
  init_formats();
8179
+ init_direct();
7879
8180
  init_render();
7880
8181
  init_commands();
7881
8182
  init_editor();
@@ -8004,6 +8305,7 @@ function clearTerminalTitle(out = titleStream()) {
8004
8305
 
8005
8306
  // src/main.ts
8006
8307
  init_version();
8308
+ init_effort();
8007
8309
  var c = palette({ isTTY: process.stdout.isTTY, env: process.env });
8008
8310
  async function main() {
8009
8311
  setTerminalTitle();
@@ -8300,6 +8602,7 @@ async function printCmd(client, config, task) {
8300
8602
  try {
8301
8603
  const res = await runAgent(client, task, {
8302
8604
  model: config.model,
8605
+ effort: savedEffort(config, config.model),
8303
8606
  root: process.cwd(),
8304
8607
  systemPrompt: collectContext(process.cwd()).systemPrompt + (capabilities?.web_search ? "" : `
8305
8608
 
@@ -8445,6 +8748,7 @@ async function runTurn(client, config, messages) {
8445
8748
  let started = false;
8446
8749
  try {
8447
8750
  const result = await client.chatStream(messages, config.model, {
8751
+ effort: savedEffort(config, config.model),
8448
8752
  onDelta: (chunk) => {
8449
8753
  if (!started) process.stdout.write(c.green("assistant "));
8450
8754
  started = true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clixad",
3
- "version": "0.0.1-beta.24",
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",