@spendgraph/prompt 0.8.1 → 0.8.2
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/package.json +5 -7
- package/docs/caching.mdx +0 -46
- package/docs/calling.mdx +0 -58
- package/docs/datasets.mdx +0 -61
- package/docs/fields.mdx +0 -63
- package/docs/overview.mdx +0 -80
- package/docs/running.mdx +0 -55
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spendgraph/prompt",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.2",
|
|
4
4
|
"description": "Pull a stored prompt or build one in code, render it, and record what it cost.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -27,7 +27,6 @@
|
|
|
27
27
|
"types": "./dist/index.d.ts",
|
|
28
28
|
"import": "./dist/index.js"
|
|
29
29
|
},
|
|
30
|
-
"./docs/*": "./docs/*",
|
|
31
30
|
"./render": {
|
|
32
31
|
"types": "./dist/render/index.d.ts",
|
|
33
32
|
"import": "./dist/render/index.js"
|
|
@@ -40,16 +39,15 @@
|
|
|
40
39
|
},
|
|
41
40
|
"files": [
|
|
42
41
|
"dist",
|
|
43
|
-
"docs",
|
|
44
42
|
"README.md"
|
|
45
43
|
],
|
|
46
44
|
"dependencies": {
|
|
47
|
-
"@spendgraph/sdk": "^0.8.
|
|
45
|
+
"@spendgraph/sdk": "^0.8.2"
|
|
48
46
|
},
|
|
49
47
|
"devDependencies": {
|
|
50
|
-
"@spendgraph/config": "0.8.
|
|
51
|
-
"@spendgraph/llms": "^0.8.
|
|
52
|
-
"@spendgraph/tools": "^0.8.
|
|
48
|
+
"@spendgraph/config": "0.8.2",
|
|
49
|
+
"@spendgraph/llms": "^0.8.2",
|
|
50
|
+
"@spendgraph/tools": "^0.8.2",
|
|
53
51
|
"typescript": "^5"
|
|
54
52
|
},
|
|
55
53
|
"engines": {
|
package/docs/caching.mdx
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
export const meta = {
|
|
2
|
-
title: "Caching: spendgraph docs",
|
|
3
|
-
description:
|
|
4
|
-
"`pull` serves a stale copy immediately and refreshes behind the caller. Invalidating has to drop both handles, because the same prompt sits under its uuid and its slug at once.",
|
|
5
|
-
};
|
|
6
|
-
|
|
7
|
-
# Caching, and invalidating it
|
|
8
|
-
|
|
9
|
-
`pull` serves a stale copy immediately and refreshes behind the caller, so an edit takes effect within a TTL without any request paying for the fetch.
|
|
10
|
-
|
|
11
|
-
## Invalidating drops both handles
|
|
12
|
-
|
|
13
|
-
`promote` invalidates for you. Doing it by hand needs to drop **both** handles, not just the one passed: `pull` keys on whatever string it was given, so the same prompt sits under its uuid and its slug at once, and `promote` is only reachable with a uuid while the dashboard teaches pulling by slug. Dropping one left the other warm for a full TTL, still rendering the wording the promotion replaced.
|
|
14
|
-
|
|
15
|
-
```ts
|
|
16
|
-
await prompts.promote(promptId, versionId); // invalidates
|
|
17
|
-
prompts.invalidate(promptId); // both handles
|
|
18
|
-
prompts.invalidate(); // everything
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
The delete runs even when nothing is stored under the handle: a cold isolate has a fetch in flight and no entry yet, and the delete is what stops that in-flight result being stored **after** the promotion.
|
|
22
|
-
|
|
23
|
-
## A handle that stopped resolving
|
|
24
|
-
|
|
25
|
-
A rename re-mints the slug, so a handle that 404s is routine rather than exotic. The cache backs one off after a failure rather than firing a doomed request on every pull, and keeps serving the entry it has.
|
|
26
|
-
|
|
27
|
-
`onPullError` is worth setting. Without it a background refresh that has stopped resolving is silent, and a rename on the dashboard freezes a running service with nothing anywhere saying so.
|
|
28
|
-
|
|
29
|
-
```ts
|
|
30
|
-
new PromptClient({
|
|
31
|
-
apiKey,
|
|
32
|
-
baseUrl,
|
|
33
|
-
onPullError: (err, key) => log.warn({ key }, String(err)),
|
|
34
|
-
cache: { ttlSeconds: 300, retryAfterSeconds: 60 },
|
|
35
|
-
});
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
`cache.identify` is not yours to set, the client fixes it to the prompt's id and slug, because an invalidation arriving under one handle has to reach a fetch started under the other.
|
|
39
|
-
|
|
40
|
-
<Callout tone="trap" title="The dataset is not cached with the prompt">
|
|
41
|
-
Cases change independently of the wording, so they are fetched fresh. A cached prompt and a stale case list would score today's answers against yesterday's questions and report it as an improvement.
|
|
42
|
-
</Callout>
|
|
43
|
-
|
|
44
|
-
## A reply that is not a prompt
|
|
45
|
-
|
|
46
|
-
`pull` refuses a response carrying no prompt, naming the handle and the two things worth checking: that the base URL points at a spendgraph app, and that the key can read this project. A prompt that is thin but real is still served, with the absent parts as their empty values.
|
package/docs/calling.mdx
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
export const meta = {
|
|
2
|
-
title: "Calling a prompt: spendgraph docs",
|
|
3
|
-
description:
|
|
4
|
-
"`call` renders, runs the model call you give it, and records what happened around it. What was sent is built once and shared by the success and failure paths.",
|
|
5
|
-
};
|
|
6
|
-
|
|
7
|
-
# Calling a prompt
|
|
8
|
-
|
|
9
|
-
`call` renders, runs the model call **you** give it, and records what happened around it. This package never sees the provider response, which is why the call is handed to it rather than made by it.
|
|
10
|
-
|
|
11
|
-
```ts
|
|
12
|
-
const reply = await stored.call({ audience: "students", max_words: 50 }, async ({ messages }) => {
|
|
13
|
-
const res = await anthropic.messages.create({ model, max_tokens: 400, messages });
|
|
14
|
-
return { output: res.content[0].text, model, inputTokens: 400, outputTokens: 12 };
|
|
15
|
-
});
|
|
16
|
-
```
|
|
17
|
-
|
|
18
|
-
It fills in the version, fields, messages, latency and status; you return the text, model and tokens.
|
|
19
|
-
|
|
20
|
-
**Return the tokens** or every cost on the prompt reads as zero, the first call that forgets warns once.
|
|
21
|
-
|
|
22
|
-
## One record, two paths
|
|
23
|
-
|
|
24
|
-
What was sent is built **once** and shared by the success path and the failure path, so the two cannot disagree about it. That is the thing hand-written reporting gets wrong.
|
|
25
|
-
|
|
26
|
-
A callback that throws is recorded as failed and then rethrown: a provider call that throws is otherwise recorded nowhere at all, which leaves the run most in need of explaining with no evidence.
|
|
27
|
-
|
|
28
|
-
## Recording
|
|
29
|
-
|
|
30
|
-
Recording is on for every prompt and is fire-and-forget. There is no client-level switch: pass `{ report: false }` on the call that should not be recorded, which is where you know whether it should.
|
|
31
|
-
|
|
32
|
-
Fire-and-forget means nothing catches a send that fails, and what is lost is spend nothing downstream can reconstruct. **`onReportError` is where that arrives**, on the client for a rollout, and on `buildCustomPrompt` for a custom prompt's usage. A client passes its own down to what it builds.
|
|
33
|
-
|
|
34
|
-
## Tools
|
|
35
|
-
|
|
36
|
-
`call` and `invoke` accept anything that hands out a turn: [`toolbus()`](/spendgraph/tools/overview), or your own.
|
|
37
|
-
|
|
38
|
-
```ts
|
|
39
|
-
await prompt.call(values, ({ messages, turn }) => call(messages, turn), { tools: bus });
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
The callback gets the turn **fully typed**; the rollout gets the tools offered and the steps taken. Which tools were offered is recorded as well as which were called: without it a rollout cannot tell "chose not to" from "was never offered", which are opposite bugs.
|
|
43
|
-
|
|
44
|
-
<Callout tone="trap" title="The recording never blocks the call, and never breaks it">
|
|
45
|
-
Reporting happens beside your call, not in front of it: a report that fails reaches `onReportError` rather than your `catch`. That is deliberate: a metering outage must not take an app down, but it does mean a missing rollout is silent unless you wired that callback.
|
|
46
|
-
</Callout>
|
|
47
|
-
|
|
48
|
-
## Binding a model
|
|
49
|
-
|
|
50
|
-
```ts
|
|
51
|
-
const chain = prompt.bind(model);
|
|
52
|
-
|
|
53
|
-
await chain.invoke(values);
|
|
54
|
-
await chain.stream(values, { onText: (delta) => res.write(delta) });
|
|
55
|
-
await chain.batch(cases, { concurrency: 4 });
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
`stream` falls back to `call` for a model that cannot stream, so binding one does not require it.
|
package/docs/datasets.mdx
DELETED
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
export const meta = {
|
|
2
|
-
title: "Versions and datasets: spendgraph docs",
|
|
3
|
-
description:
|
|
4
|
-
"Cases sit on one side of a wall the server decides, so a case never moves sides. SetCases replaces the whole set, because a dataset has one answer at a time.",
|
|
5
|
-
};
|
|
6
|
-
|
|
7
|
-
# Versions and datasets
|
|
8
|
-
|
|
9
|
-
```ts
|
|
10
|
-
await prompts.versions(promptId, { origin: "user", limit: 20 });
|
|
11
|
-
await prompts.cases(promptId);
|
|
12
|
-
await prompts.setCases(promptId, cases);
|
|
13
|
-
```
|
|
14
|
-
|
|
15
|
-
## A case sits on one side of a wall
|
|
16
|
-
|
|
17
|
-
Every case is either `feedback` or `held_out`, and **the server decides which**: it is not a field you set.
|
|
18
|
-
|
|
19
|
-
```
|
|
20
|
-
feedback ──▶ what an optimiser is allowed to read and learn from
|
|
21
|
-
held_out ──▶ what it never sees, so a score means something
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
That is the whole point of the split, and the reason the client cannot set it. A case that could move sides is a case that can be quietly moved to the side where the prompt already does well, and every number downstream stops meaning anything. `DatasetCase.split` exists on the type so a read can report it; omit it on a write.
|
|
25
|
-
|
|
26
|
-
```ts
|
|
27
|
-
const summary = await prompts.cases(promptId);
|
|
28
|
-
|
|
29
|
-
summary.counts; // { total, feedback, held_out }
|
|
30
|
-
summary.problem; // why this cannot be optimised against yet, or null
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
`problem` is the useful one. A dataset too small, too lopsided, or missing expected outputs cannot support a comparison, and the server says which rather than returning a confident score built on four cases.
|
|
34
|
-
|
|
35
|
-
## `setCases` replaces everything
|
|
36
|
-
|
|
37
|
-
Not a merge, and deliberately.
|
|
38
|
-
|
|
39
|
-
A dataset is a **set**: "which cases am I evaluating against" has one answer at a time. A merge would leave no way to remove a case, and a stale case nobody can delete quietly biases every run after it.
|
|
40
|
-
|
|
41
|
-
```ts
|
|
42
|
-
await prompts.setCases(promptId, [
|
|
43
|
-
{ caseId: "refund-partial", fieldValues: { audience: "students" }, expected: "…" },
|
|
44
|
-
]);
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
Send the set you want to have. Read it back with `cases()` if you need to confirm what landed.
|
|
48
|
-
|
|
49
|
-
<Callout tone="trap" title="A case id is not a label: it decides which side of the wall the case lands on">
|
|
50
|
-
The split is derived from the id, so renaming a case re-rolls it: a case that was held out can come back as feedback, and whatever proposes changes may now read what it is scored against. Nothing in the data would say so. Change the text, keep the id.
|
|
51
|
-
</Callout>
|
|
52
|
-
|
|
53
|
-
## Versions
|
|
54
|
-
|
|
55
|
-
```ts
|
|
56
|
-
await prompts.versions(promptId, { origin: "user", limit: 20 });
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
`origin` separates a version somebody wrote from one a run produced, which matters once an optimiser is writing candidates: otherwise the history reads as though a person made forty edits in a minute.
|
|
60
|
-
|
|
61
|
-
Promoting a version is on [Caching](/spendgraph/prompt/caching), because the part worth knowing is what it invalidates.
|
package/docs/fields.mdx
DELETED
|
@@ -1,63 +0,0 @@
|
|
|
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.
|
package/docs/overview.mdx
DELETED
|
@@ -1,80 +0,0 @@
|
|
|
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
|
-
<PromptSources />
|
|
20
|
-
|
|
21
|
-
`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.
|
|
22
|
-
|
|
23
|
-
```ts
|
|
24
|
-
import { PromptClient, buildCustomPrompt } from "@spendgraph/prompt";
|
|
25
|
-
|
|
26
|
-
const prompts = new PromptClient({
|
|
27
|
-
apiKey: process.env.SPENDGRAPH_API_KEY,
|
|
28
|
-
baseUrl: "https://spendgraph.locusgraph.com",
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
const stored = await prompts.pull("billing-explainer");
|
|
32
|
-
|
|
33
|
-
const local = buildCustomPrompt({
|
|
34
|
-
name: "billing-explainer",
|
|
35
|
-
blocks: [{ title: "Role", body: "You explain invoices to {audience}." }],
|
|
36
|
-
question: "Explain this line item in {max_words} words.",
|
|
37
|
-
fields: [
|
|
38
|
-
{ name: "audience", type: "string", required: true },
|
|
39
|
-
{ name: "max_words", type: "number", required: true },
|
|
40
|
-
],
|
|
41
|
-
});
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
`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.
|
|
45
|
-
|
|
46
|
-
## Running one
|
|
47
|
-
|
|
48
|
-
Bind a model and the prompt runs itself:
|
|
49
|
-
|
|
50
|
-
```ts
|
|
51
|
-
import { Llm } from "@spendgraph/llms";
|
|
52
|
-
|
|
53
|
-
const chain = stored.bind(new Llm({ client: anthropic, model: "claude-opus-5" }));
|
|
54
|
-
|
|
55
|
-
const answer = await chain.invoke({ audience: "students", max_words: 50 });
|
|
56
|
-
answer.output;
|
|
57
|
-
answer.rolloutId;
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
`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.
|
|
61
|
-
|
|
62
|
-
## What each kind records
|
|
63
|
-
|
|
64
|
-
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.
|
|
65
|
-
|
|
66
|
-
Read `prompt.source` to tell them apart.
|
|
67
|
-
|
|
68
|
-
<Callout tone="trap" title="A custom prompt has no version, so its spend is usage rather than a rollout">
|
|
69
|
-
`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.
|
|
70
|
-
</Callout>
|
|
71
|
-
|
|
72
|
-
## Where to go next
|
|
73
|
-
|
|
74
|
-
| | |
|
|
75
|
-
| --- | --- |
|
|
76
|
-
| [Fields](/spendgraph/prompt/fields) | placeholders, types, and what is substituted |
|
|
77
|
-
| [Calling a prompt](/spendgraph/prompt/calling) | `call`, history, tools, and what gets recorded |
|
|
78
|
-
| [Caching](/spendgraph/prompt/caching) | serving stale, refreshing behind, invalidating both handles |
|
|
79
|
-
| [Running server-side](/spendgraph/prompt/running) | `run`, `sample`, `runAll`, and the spend ceiling |
|
|
80
|
-
| [Versions and datasets](/spendgraph/prompt/datasets) | the split the server decides, and why it does |
|
package/docs/running.mdx
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
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](/spendgraph/prompt/datasets).
|