@gafj/gafj 0.1.7 → 0.1.9

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.
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
 
3
3
  const DEFAULT_URL = "https://api.anthropic.com";
4
- const DEFAULT_MODEL = "claude-fable-5-1";
4
+ const DEFAULT_MODEL = "claude-sonnet-5";
5
5
 
6
6
  async function complete({ provider, system, user, max_tokens = 4000, fetch }) {
7
7
  const { post } = require("./index");
@@ -22,7 +22,8 @@ function checkUrl(url) {
22
22
  return u.toString();
23
23
  }
24
24
 
25
- async function post(fetchImpl, url, headers, body, timeoutMs = 120000) {
25
+ // a long resume with a span for every quoted phrase can take a model several minutes; the key test passes its own short limit
26
+ async function post(fetchImpl, url, headers, body, timeoutMs = 600000) {
26
27
  const f = fetchImpl || globalThis.fetch;
27
28
  const ac = new AbortController();
28
29
  const t = setTimeout(() => ac.abort(), timeoutMs);
package/http/api.js CHANGED
@@ -154,9 +154,14 @@ route("POST", "/api/kb/questions/:id/dismiss", (c, p) => discover.dismissQuestio
154
154
 
155
155
  // usage: tokens per operation from the attempts the app recorded; the pricing test reads this
156
156
  route("GET", "/api/usage", (c) => ({
157
- by_op: c.db.prepare(`SELECT r.operation AS op, count(*) AS attempts, sum(CASE WHEN a.result = 'passed' THEN 1 ELSE 0 END) AS passed,
157
+ by_op: c.db.prepare(`SELECT r.operation AS op, count(*) AS attempts,
158
+ sum(CASE WHEN a.result = 'passed' THEN 1 ELSE 0 END) AS passed, sum(CASE WHEN a.result <> 'passed' THEN 1 ELSE 0 END) AS failed,
159
+ coalesce(sum(CASE WHEN a.result = 'passed' THEN a.tokens_in END), 0) AS in_passed, coalesce(sum(CASE WHEN a.result = 'passed' THEN a.tokens_out END), 0) AS out_passed,
160
+ coalesce(sum(CASE WHEN a.result <> 'passed' THEN a.tokens_in END), 0) AS in_failed, coalesce(sum(CASE WHEN a.result <> 'passed' THEN a.tokens_out END), 0) AS out_failed,
158
161
  coalesce(sum(a.tokens_in), 0) AS tokens_in, coalesce(sum(a.tokens_out), 0) AS tokens_out, coalesce(avg(a.latency_ms), 0) AS avg_ms, max(a.model) AS model
159
162
  FROM ai_attempt a JOIN ai_run r ON r.id = a.ai_run_id WHERE r.candidate_id = ? AND a.tokens_in IS NOT NULL GROUP BY r.operation ORDER BY r.operation`).all(c.candidate_id),
163
+ // calls that never returned (a timeout, a network drop) recorded no tokens here but were still billed by the provider
164
+ unrecorded: c.db.prepare("SELECT count(*) c FROM ai_run r WHERE r.candidate_id = ? AND r.route IN ('byo_key', 'credits') AND NOT EXISTS (SELECT 1 FROM ai_attempt a WHERE a.ai_run_id = r.id)").get(c.candidate_id).c,
160
165
  since: c.db.prepare("SELECT min(a.started_at) AS s FROM ai_attempt a JOIN ai_run r ON r.id = a.ai_run_id WHERE r.candidate_id = ? AND a.tokens_in IS NOT NULL").get(c.candidate_id).s,
161
166
  }));
162
167
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gafj/gafj",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "GAF-J: a local campaign engine for job search. One SQLite file, one ingest door, your own AI subscription.",
5
5
  "homepage": "https://gaf-j.com",
6
6
  "repository": {
package/ui/screens/kb.js CHANGED
@@ -52,7 +52,7 @@ export function Onboarding({ d, reload, setErr, route, compact }) {
52
52
  const groups = b ? b.groups.map((g) => ({ id: g, rows: b.pending.filter((p) => p.dedup_group_id === g) })) : [];
53
53
  return html`<div class="card">
54
54
  <div class="row"><input type="file" multiple accept=".docx,.pdf,.md,.txt" disabled=${busy} onChange=${upload} /><span class="small muted">docx, pdf, md, txt; text is extracted locally, no OCR</span></div>
55
- ${direct && notYet > 1 ? html`<div class="row"><button class="primary" disabled=${!!all} onClick=${extractAll}>${all ? "Extracting " + all : `Extract all ${notYet} files`}</button><span class="small muted">one after another through your AI; a few seconds each</span></div>` : ""}
55
+ ${direct && notYet > 1 ? html`<div class="row"><button class="primary" disabled=${!!all} onClick=${extractAll}>${all ? "Extracting " + all : `Extract all ${notYet} files`}</button><span class="small muted">one after another through your AI; one to three minutes each for a full resume</span></div>` : ""}
56
56
  ${!direct && notYet > 1 ? html`<p class="small muted">On the paste route each file is one packet you carry to your chat and back; with a provider key in Settings the whole pile runs in one click.</p>` : ""}
57
57
  ${b ? html`<ul class="list">${b.sources.map((s) => html`<li key=${s.source_document_id}><b>${s.filename}</b> <span class="tag">${s.kind}</span>
58
58
  ${s.empty ? html`<span class="tag blocked">no text</span>` : html`<span class="muted small">${s.chars} chars</span>`}
@@ -5,7 +5,7 @@ import { api, useInstall } from "../lib.js";
5
5
 
6
6
  const KINDS = ["resume", "cover", "onepager", "email", "prep", "deep_answers", "practice", "cheatsheet"];
7
7
  const KIND_LABEL = { anthropic: "Anthropic (Claude)", openai_compatible: "OpenAI, or any OpenAI-compatible server", gemini: "Google Gemini" };
8
- const KIND_MODEL = { anthropic: "claude-fable-5-1" };
8
+ const KIND_MODEL = { anthropic: "claude-sonnet-5" };
9
9
 
10
10
  export function Settings({ live }) {
11
11
  const [s, setS] = useState(null);
@@ -92,10 +92,14 @@ export function Settings({ live }) {
92
92
  </div>
93
93
  <h2>Usage</h2>
94
94
  <div class="card">
95
- ${usage && usage.by_op.length ? html`<table class="cats"><thead><tr><th>operation</th><th>runs</th><th>passed</th><th>tokens in</th><th>tokens out</th><th>avg seconds</th><th>model</th></tr></thead><tbody>
96
- ${usage.by_op.map((u) => html`<tr key=${u.op}><td>${u.op}</td><td>${u.attempts}</td><td>${u.passed}</td><td>${u.tokens_in.toLocaleString()}</td><td>${u.tokens_out.toLocaleString()}</td><td>${(u.avg_ms / 1000).toFixed(1)}</td><td class="small muted">${u.model || ""}</td></tr>`)}
97
- <tr><td><b>total</b></td><td>${usage.by_op.reduce((n, u) => n + u.attempts, 0)}</td><td>${usage.by_op.reduce((n, u) => n + u.passed, 0)}</td><td><b>${usage.by_op.reduce((n, u) => n + u.tokens_in, 0).toLocaleString()}</b></td><td><b>${usage.by_op.reduce((n, u) => n + u.tokens_out, 0).toLocaleString()}</b></td><td></td><td></td></tr>
98
- </tbody></table><p class="small muted">Counted from every run through your own provider or credits since ${usage.since ? new Date(usage.since).toLocaleDateString() : "the start"}. Paste runs are not counted; the app never sees those token counts. Multiply by your provider's price per million tokens for the cost.</p>`
95
+ ${usage && usage.by_op.length ? html`<table class="cats"><thead><tr><th>operation</th><th>result</th><th>runs</th><th>tokens in</th><th>tokens out</th><th>model</th></tr></thead><tbody>
96
+ ${usage.by_op.flatMap((u) => [
97
+ html`<tr key=${u.op + "p"}><td rowspan="2"><b>${u.op}</b><div class="small muted">${(u.avg_ms / 1000).toFixed(0)} s avg</div></td><td><span class="tag passed">passed</span></td><td>${u.passed}</td><td>${u.in_passed.toLocaleString()}</td><td>${u.out_passed.toLocaleString()}</td><td rowspan="2" class="small muted">${u.model || ""}</td></tr>`,
98
+ html`<tr key=${u.op + "f"}><td><span class="tag blocked">failed</span></td><td>${u.failed}</td><td>${u.in_failed.toLocaleString()}</td><td>${u.out_failed.toLocaleString()}</td></tr>`,
99
+ ])}
100
+ <tr><td><b>total</b></td><td></td><td>${usage.by_op.reduce((n, u) => n + u.attempts, 0)}</td><td><b>${usage.by_op.reduce((n, u) => n + u.tokens_in, 0).toLocaleString()}</b></td><td><b>${usage.by_op.reduce((n, u) => n + u.tokens_out, 0).toLocaleString()}</b></td><td></td></tr>
101
+ <tr><td colspan="2"><b>of which wasted on failures</b></td><td>${usage.by_op.reduce((n, u) => n + u.failed, 0)}</td><td>${usage.by_op.reduce((n, u) => n + u.in_failed, 0).toLocaleString()}</td><td>${usage.by_op.reduce((n, u) => n + u.out_failed, 0).toLocaleString()}</td><td></td></tr>
102
+ </tbody></table><p class="small muted">Counted from every run through your own provider or credits since ${usage.since ? new Date(usage.since).toLocaleDateString() : "the start"}. "Failed" is a reply that came back but did not pass the gate. ${usage.unrecorded ? `${usage.unrecorded} call${usage.unrecorded === 1 ? "" : "s"} never returned (a timeout or a dropped connection): no tokens recorded here, but the provider still billed the input.` : ""} Paste runs are not counted. Multiply by your provider's price per million tokens for the cost.</p>`
99
103
  : html`<p class="small muted">Nothing counted yet. Runs through your own provider key or credits record their token counts here.</p>`}
100
104
  </div>
101
105
  <h2>Bullet register</h2>