@spendgraph/prompt 0.5.0 → 0.7.0

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 (70) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +27 -221
  3. package/dist/budget/budget.d.ts +10 -0
  4. package/dist/budget/budget.js +1 -31
  5. package/dist/budget/errors.js +1 -11
  6. package/dist/budget/index.js +1 -2
  7. package/dist/build/build.d.ts +11 -2
  8. package/dist/build/build.js +1 -57
  9. package/dist/build/index.js +1 -1
  10. package/dist/cache/cache.d.ts +16 -0
  11. package/dist/cache/cache.js +1 -128
  12. package/dist/cache/index.js +1 -1
  13. package/dist/client.d.ts +27 -1
  14. package/dist/client.js +1 -145
  15. package/dist/fingerprint/fingerprint.d.ts +23 -0
  16. package/dist/fingerprint/fingerprint.js +1 -0
  17. package/dist/fingerprint/index.d.ts +2 -0
  18. package/dist/fingerprint/index.js +1 -0
  19. package/dist/index.d.ts +5 -3
  20. package/dist/index.js +1 -5
  21. package/dist/internals.d.ts +4 -2
  22. package/dist/internals.js +1 -6
  23. package/dist/pack/index.d.ts +3 -0
  24. package/dist/pack/index.js +1 -0
  25. package/dist/pack/pack.d.ts +58 -0
  26. package/dist/pack/pack.js +1 -0
  27. package/dist/pack/tokens.d.ts +11 -0
  28. package/dist/pack/tokens.js +1 -0
  29. package/dist/prompt/bind.d.ts +13 -18
  30. package/dist/prompt/bind.js +1 -15
  31. package/dist/prompt/factory.d.ts +1 -1
  32. package/dist/prompt/factory.js +1 -100
  33. package/dist/prompt/index.js +1 -2
  34. package/dist/pull/index.js +1 -1
  35. package/dist/pull/pull.js +1 -19
  36. package/dist/render/index.d.ts +1 -1
  37. package/dist/render/index.js +1 -3
  38. package/dist/render/messages.js +1 -14
  39. package/dist/render/record.d.ts +12 -0
  40. package/dist/render/record.js +1 -0
  41. package/dist/render/system.js +4 -13
  42. package/dist/render/types.d.ts +21 -5
  43. package/dist/render/types.js +0 -1
  44. package/dist/render/variables.d.ts +7 -0
  45. package/dist/render/variables.js +1 -20
  46. package/dist/run/across-models.d.ts +11 -1
  47. package/dist/run/across-models.js +1 -5
  48. package/dist/run/concurrency.d.ts +1 -7
  49. package/dist/run/concurrency.js +1 -12
  50. package/dist/run/id.js +1 -3
  51. package/dist/run/index.js +1 -5
  52. package/dist/run/once.js +1 -21
  53. package/dist/run/sample.js +1 -28
  54. package/dist/types/dataset.js +0 -1
  55. package/dist/types/index.js +0 -1
  56. package/dist/types/payload.js +0 -1
  57. package/dist/types/prompt.d.ts +6 -1
  58. package/dist/types/prompt.js +0 -1
  59. package/dist/types/result.js +0 -1
  60. package/dist/types/rollout.js +0 -1
  61. package/dist/types/run.js +0 -1
  62. package/dist/types/trace.js +0 -1
  63. package/dist/types/version.js +0 -1
  64. package/docs/caching.mdx +46 -0
  65. package/docs/calling.mdx +58 -0
  66. package/docs/datasets.mdx +61 -0
  67. package/docs/fields.mdx +63 -0
  68. package/docs/overview.mdx +78 -0
  69. package/docs/running.mdx +55 -0
  70. package/package.json +12 -11
@@ -0,0 +1,63 @@
1
+ export const meta = {
2
+ title: "Fields — spendgraph docs",
3
+ description:
4
+ "A placeholder is declared as a FieldSpec. A missing required field throws before anything is sent, and a value with nowhere to land is said rather than dropped.",
5
+ };
6
+
7
+ # Fields
8
+
9
+ A `{placeholder}` is declared as a `FieldSpec` — a name, a type, whether it is required, and how it renders. A missing required field throws **before anything is sent**.
10
+
11
+ What it throws is `FieldValidationError`, re-exported from the SDK so a caller has one name to catch whether the failure came from a prompt, a tool argument or a graph input:
12
+
13
+ ```ts
14
+ import { FieldValidationError } from "@spendgraph/prompt";
15
+
16
+ try {
17
+ prompt.format(values);
18
+ } catch (err) {
19
+ if (err instanceof FieldValidationError) err.errors; // which fields, and why
20
+ }
21
+ ```
22
+
23
+ ```ts
24
+ prompt.format(values); // Message[], validated and substituted
25
+ prompt.serialize(values); // the field map that format used
26
+ ```
27
+
28
+ Types come from `@spendgraph/sdk`, because a tool's arguments and a graph node's inputs are the same problem.
29
+
30
+ ## Only declared fields are substituted
31
+
32
+ A `{placeholder}` nobody supplied is **left as it is**. Emptying it would send a prompt with a hole the caller cannot see, and noise that reads as content is worse than a visible gap.
33
+
34
+ A value you *did* pass with no field to land in is a different thing. The first time that happens, the prompt says so:
35
+
36
+ ```
37
+ [prompt] {audience} appears in this prompt and you passed a value for it, but no
38
+ field declares it, so it was not substituted and the placeholder goes to the
39
+ model as written. Add it to the prompt's fields.
40
+ ```
41
+
42
+ All three conditions have to hold — the value was supplied, the placeholder is in the wording, and no field declares it — so an unrelated value and an unsupplied placeholder both stay quiet.
43
+
44
+ ## Substitution reads own properties only
45
+
46
+ `{constructor}`, `{toString}` and `{valueOf}` are not supplied just because every object has them. A template about code is all it takes to send `explain what function Object() { [native code] } does` to a model, and that is noise dressed as content.
47
+
48
+ <Callout tone="trap" title="A value that looks like a placeholder stays text">
49
+ Substitution runs once and does not re-read what it wrote, so a field whose value is `{other}` reaches the model as those eight characters. The alternative is a value from a user deciding which of your other fields to expand.
50
+ </Callout>
51
+
52
+ ## Conversation history
53
+
54
+ Prior turns render between the system turn and the question, oldest first.
55
+
56
+ ```ts
57
+ prompt.format(values, { history });
58
+ await prompt.call(values, ({ messages }) => call(messages), { history });
59
+ ```
60
+
61
+ History is passed through **verbatim** — it is a record of what was already said, not a template, so `{placeholders}` inside it are left alone. A `system` turn in the history is dropped: the prompt owns that turn, and two of them is a different request from the one the prompt describes.
62
+
63
+ A rollout records at most 50 messages, which is what the route accepts. A longer conversation still runs; only its rollout is refused, and that refusal reaches `onReportError` rather than your call — so the first time it happens the package warns, once, while somebody is still watching. `MAX_RECORDED_MESSAGES` is exported if you want to trim before it bites.
@@ -0,0 +1,78 @@
1
+ export const meta = {
2
+ title: "Prompts — spendgraph docs",
3
+ description:
4
+ "Pull a stored prompt or write one in code, render it, and record what it cost. Both hand back the same object, so nothing downstream has to know which it was given.",
5
+ };
6
+
7
+ # Prompts
8
+
9
+ The wording that produced last week's answer is not the wording in the file today, and a prompt edited since is a score nobody can reproduce.
10
+
11
+ Pull a prompt from the dashboard or write one in code, render it, and record what the call cost — with the exact messages that were sent kept beside the reply.
12
+
13
+ ```sh
14
+ npm install @spendgraph/prompt
15
+ ```
16
+
17
+ ## Two ways in, one prompt
18
+
19
+ `pullPrompt` fetches the wording from the server. `buildCustomPrompt` declares it in code. Both hand back the same `Prompt`, so `format`, `serialize` and `call` behave identically and nothing downstream has to know which it was given.
20
+
21
+ ```ts
22
+ import { PromptClient, buildCustomPrompt } from "@spendgraph/prompt";
23
+
24
+ const prompts = new PromptClient({
25
+ apiKey: process.env.SPENDGRAPH_API_KEY,
26
+ baseUrl: "https://spendgraph.locusgraph.com",
27
+ });
28
+
29
+ const stored = await prompts.pull("billing-explainer");
30
+
31
+ const local = buildCustomPrompt({
32
+ name: "billing-explainer",
33
+ blocks: [{ title: "Role", body: "You explain invoices to {audience}." }],
34
+ question: "Explain this line item in {max_words} words.",
35
+ fields: [
36
+ { name: "audience", type: "string", required: true },
37
+ { name: "max_words", type: "number", required: true },
38
+ ],
39
+ });
40
+ ```
41
+
42
+ `pull` takes an id or a slug — the server resolves either. `pullPrompt` is the same fetch as a standalone function, for a caller that wants one prompt and no client to hold.
43
+
44
+ ## Running one
45
+
46
+ Bind a model and the prompt runs itself:
47
+
48
+ ```ts
49
+ import { Llm } from "@spendgraph/llms";
50
+
51
+ const chain = stored.bind(new Llm({ client: anthropic, model: "claude-opus-5" }));
52
+
53
+ const answer = await chain.invoke({ audience: "students", max_words: 50 });
54
+ answer.output;
55
+ answer.rolloutId;
56
+ ```
57
+
58
+ `bind` asks for a **shape, not an import** — anything with `call(messages)` returning an outcome satisfies `Model`, and an `LlmReply` already is one. Neither package depends on the other.
59
+
60
+ ## What each kind records
61
+
62
+ A **server** prompt has a row and a version behind it, so a call writes a rollout. A **custom** prompt has neither, so it records usage instead: the tokens still land on the dashboard, attributed to the model and tagged with the prompt's name; only the wording goes unversioned.
63
+
64
+ Read `prompt.source` to tell them apart.
65
+
66
+ <Callout tone="trap" title="A custom prompt has no version, so its spend is usage rather than a rollout">
67
+ `buildCustomPrompt` records the call and the cost exactly as a stored one does — but there is no row and no version, so nothing ties the number to a wording you can go back to. Fine while you are exploring; not what you want under a score you intend to compare next month.
68
+ </Callout>
69
+
70
+ ## Where to go next
71
+
72
+ | | |
73
+ | --- | --- |
74
+ | [Fields](/docs/prompt/fields) | placeholders, types, and what is substituted |
75
+ | [Calling a prompt](/docs/prompt/calling) | `call`, history, tools, and what gets recorded |
76
+ | [Caching](/docs/prompt/caching) | serving stale, refreshing behind, invalidating both handles |
77
+ | [Running server-side](/docs/prompt/running) | `run`, `sample`, `runAll`, and the spend ceiling |
78
+ | [Versions and datasets](/docs/prompt/datasets) | the split the server decides, and why it does |
@@ -0,0 +1,55 @@
1
+ export const meta = {
2
+ title: "Running server-side — spendgraph docs",
3
+ description:
4
+ "run, sample and runAll execute the prompt where the spec, rendering and pricing already live. The batch path, and the ceiling that stops it running away.",
5
+ };
6
+
7
+ # Running server-side
8
+
9
+ `run`, `sample` and `runAll` execute the prompt **on the server**, where the spec, rendering and pricing already live.
10
+
11
+ This is the batch path. A production request should pull, render and report instead: the server-side call adds a hop and makes spendgraph a dependency of your uptime, which is the right trade for evaluation and the wrong one for a user waiting on a response.
12
+
13
+ ```ts
14
+ await prompts.run(promptId, values);
15
+ await prompts.sample(promptId, values, { k: 5 });
16
+ await prompts.runAll(promptId, values); // one rollout per saved model
17
+ ```
18
+
19
+ ## Every seed, and every model, comes back
20
+
21
+ `sample` always returns **k results**. A failed seed is present with `status: "failed"` rather than missing — three results for k = 5 would let a metric compute over three and report better consistency than the run showed.
22
+
23
+ `runAll` does the same across models. One slug with a typo used to take the whole comparison with it — and not stop the calls already queued, which still left, landed and were billed after the caller had been told the run failed. Now each model comes back in the order asked, a failed one carrying its own name and the provider's message.
24
+
25
+ A **budget stop** is the exception: it throws `BudgetExceededError`, carrying what was spent and what the ceiling was. Money running out says nothing about a prompt or a model, and recording those as failures would put the wallet into `pass^k`.
26
+
27
+ ```ts
28
+ import { BudgetExceededError } from "@spendgraph/prompt";
29
+
30
+ catch (err) {
31
+ if (err instanceof BudgetExceededError) {
32
+ err.spentMicros; // integer micro-USD, as everywhere else
33
+ err.limitMicros;
34
+ }
35
+ }
36
+ ```
37
+
38
+ ## The spend ceiling
39
+
40
+ `maxCostMicros` caps what those spend. Omit it and the client warns once that it is unbounded; pass `null` to say you meant it.
41
+
42
+ ```ts
43
+ new PromptClient({ apiKey, baseUrl, maxCostMicros: 5_000_000 }); // $5
44
+ new PromptClient({ apiKey, baseUrl, maxCostMicros: null }); // deliberately none
45
+ ```
46
+
47
+ `0` is kept, because spending nothing is a thing a caller can mean. A ceiling that is `NaN` or negative is **refused**: `NaN >= NaN` is false, so a `NaN` limit never fires and the guard is silently not there — and `Number(process.env.MAX_COST_MICROS)` on an unset variable is `NaN`, which is how it arrives without anyone typing it.
48
+
49
+ <Callout tone="trap" title="Omit `maxCostMicros` and the client warns once, then spends without a ceiling">
50
+ `runAll` fans out across every saved model and `sample` multiplies that by k, so the bill is a product of two numbers neither of which is in front of you. Pass a ceiling, or pass `null` to say you meant it — the warning appears once per process and is easy to scroll past.
51
+ </Callout>
52
+
53
+ ## What you run against
54
+
55
+ Cases and versions have their own page: [Versions and datasets](/docs/prompt/datasets).
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@spendgraph/prompt",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "description": "Pull a stored prompt or build one in code, render it, and record what it cost.",
5
- "license": "MIT",
5
+ "license": "Apache-2.0",
6
6
  "repository": {
7
7
  "type": "git",
8
8
  "url": "git+https://github.com/fnLog0/spendgraph.git",
@@ -27,6 +27,7 @@
27
27
  "types": "./dist/index.d.ts",
28
28
  "import": "./dist/index.js"
29
29
  },
30
+ "./docs/*": "./docs/*",
30
31
  "./render": {
31
32
  "types": "./dist/render/index.d.ts",
32
33
  "import": "./dist/render/index.js"
@@ -39,19 +40,15 @@
39
40
  },
40
41
  "files": [
41
42
  "dist",
43
+ "docs",
42
44
  "README.md"
43
45
  ],
44
- "scripts": {
45
- "build": "tsc -p tsconfig.json --emitDeclarationOnly && tsc -p tsconfig.json --declaration false --removeComments",
46
- "test": "npm run build && vitest run",
47
- "pretest": "npm run build --workspace @spendgraph/tools"
48
- },
49
46
  "dependencies": {
50
- "@spendgraph/sdk": "^0.5.0"
47
+ "@spendgraph/sdk": "^0.7.0"
51
48
  },
52
49
  "devDependencies": {
53
- "@spendgraph/llms": "^0.5.0",
54
- "@spendgraph/tools": "^0.5.0",
50
+ "@spendgraph/llms": "^0.7.0",
51
+ "@spendgraph/tools": "^0.7.0",
55
52
  "typescript": "^5"
56
53
  },
57
54
  "engines": {
@@ -59,5 +56,9 @@
59
56
  },
60
57
  "publishConfig": {
61
58
  "access": "public"
59
+ },
60
+ "scripts": {
61
+ "build": "rm -rf dist && tsc -p tsconfig.json --emitDeclarationOnly && tsc -p tsconfig.json --declaration false --removeComments && node ../../scripts/minify.mjs dist",
62
+ "test": "vitest run"
62
63
  }
63
- }
64
+ }