@spendgraph/prompt 0.5.0 → 0.6.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.
- package/LICENSE +202 -0
- package/README.md +24 -221
- package/dist/budget/budget.d.ts +10 -0
- package/dist/budget/budget.js +1 -31
- package/dist/budget/errors.js +1 -11
- package/dist/budget/index.js +1 -2
- package/dist/build/build.d.ts +11 -2
- package/dist/build/build.js +1 -57
- package/dist/build/index.js +1 -1
- package/dist/cache/cache.js +1 -128
- package/dist/cache/index.js +1 -1
- package/dist/client.d.ts +7 -1
- package/dist/client.js +1 -145
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -5
- package/dist/internals.d.ts +2 -2
- package/dist/internals.js +1 -6
- package/dist/prompt/bind.js +1 -15
- package/dist/prompt/factory.d.ts +1 -1
- package/dist/prompt/factory.js +1 -100
- package/dist/prompt/index.js +1 -2
- package/dist/pull/index.js +1 -1
- package/dist/pull/pull.js +1 -19
- package/dist/render/index.js +1 -3
- package/dist/render/messages.js +1 -14
- package/dist/render/system.js +4 -13
- package/dist/render/types.js +0 -1
- package/dist/render/variables.d.ts +7 -0
- package/dist/render/variables.js +1 -20
- package/dist/run/across-models.d.ts +11 -1
- package/dist/run/across-models.js +1 -5
- package/dist/run/concurrency.js +1 -12
- package/dist/run/id.js +1 -3
- package/dist/run/index.js +1 -5
- package/dist/run/once.js +1 -21
- package/dist/run/sample.js +1 -28
- package/dist/types/dataset.js +0 -1
- package/dist/types/index.js +0 -1
- package/dist/types/payload.js +0 -1
- package/dist/types/prompt.d.ts +1 -1
- package/dist/types/prompt.js +0 -1
- package/dist/types/result.js +0 -1
- package/dist/types/rollout.js +0 -1
- package/dist/types/run.js +0 -1
- package/dist/types/trace.js +0 -1
- package/dist/types/version.js +0 -1
- package/package.json +7 -7
package/LICENSE
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
|
|
2
|
+
Apache License
|
|
3
|
+
Version 2.0, January 2004
|
|
4
|
+
http://www.apache.org/licenses/
|
|
5
|
+
|
|
6
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
7
|
+
|
|
8
|
+
1. Definitions.
|
|
9
|
+
|
|
10
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
11
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
12
|
+
|
|
13
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
14
|
+
the copyright owner that is granting the License.
|
|
15
|
+
|
|
16
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
17
|
+
other entities that control, are controlled by, or are under common
|
|
18
|
+
control with that entity. For the purposes of this definition,
|
|
19
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
20
|
+
direction or management of such entity, whether by contract or
|
|
21
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
22
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
23
|
+
|
|
24
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
25
|
+
exercising permissions granted by this License.
|
|
26
|
+
|
|
27
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
28
|
+
including but not limited to software source code, documentation
|
|
29
|
+
source, and configuration files.
|
|
30
|
+
|
|
31
|
+
"Object" form shall mean any form resulting from mechanical
|
|
32
|
+
transformation or translation of a Source form, including but
|
|
33
|
+
not limited to compiled object code, generated documentation,
|
|
34
|
+
and conversions to other media types.
|
|
35
|
+
|
|
36
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
37
|
+
Object form, made available under the License, as indicated by a
|
|
38
|
+
copyright notice that is included in or attached to the work
|
|
39
|
+
(an example is provided in the Appendix below).
|
|
40
|
+
|
|
41
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
42
|
+
form, that is based on (or derived from) the Work and for which the
|
|
43
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
44
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
45
|
+
of this License, Derivative Works shall not include works that remain
|
|
46
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
47
|
+
the Work and Derivative Works thereof.
|
|
48
|
+
|
|
49
|
+
"Contribution" shall mean any work of authorship, including
|
|
50
|
+
the original version of the Work and any modifications or additions
|
|
51
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
52
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
53
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
54
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
55
|
+
means any form of electronic, verbal, or written communication sent
|
|
56
|
+
to the Licensor or its representatives, including but not limited to
|
|
57
|
+
communication on electronic mailing lists, source code control systems,
|
|
58
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
59
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
60
|
+
excluding communication that is conspicuously marked or otherwise
|
|
61
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
62
|
+
|
|
63
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
64
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
65
|
+
subsequently incorporated within the Work.
|
|
66
|
+
|
|
67
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
68
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
69
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
70
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
71
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
72
|
+
Work and such Derivative Works in Source or Object form.
|
|
73
|
+
|
|
74
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
75
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
76
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
77
|
+
(except as stated in this section) patent license to make, have made,
|
|
78
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
79
|
+
where such license applies only to those patent claims licensable
|
|
80
|
+
by such Contributor that are necessarily infringed by their
|
|
81
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
82
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
83
|
+
institute patent litigation against any entity (including a
|
|
84
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
85
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
86
|
+
or contributory patent infringement, then any patent licenses
|
|
87
|
+
granted to You under this License for that Work shall terminate
|
|
88
|
+
as of the date such litigation is filed.
|
|
89
|
+
|
|
90
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
91
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
92
|
+
modifications, and in Source or Object form, provided that You
|
|
93
|
+
meet the following conditions:
|
|
94
|
+
|
|
95
|
+
(a) You must give any other recipients of the Work or
|
|
96
|
+
Derivative Works a copy of this License; and
|
|
97
|
+
|
|
98
|
+
(b) You must cause any modified files to carry prominent notices
|
|
99
|
+
stating that You changed the files; and
|
|
100
|
+
|
|
101
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
102
|
+
that You distribute, all copyright, patent, trademark, and
|
|
103
|
+
attribution notices from the Source form of the Work,
|
|
104
|
+
excluding those notices that do not pertain to any part of
|
|
105
|
+
the Derivative Works; and
|
|
106
|
+
|
|
107
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
108
|
+
distribution, then any Derivative Works that You distribute must
|
|
109
|
+
include a readable copy of the attribution notices contained
|
|
110
|
+
within such NOTICE file, excluding those notices that do not
|
|
111
|
+
pertain to any part of the Derivative Works, in at least one
|
|
112
|
+
of the following places: within a NOTICE text file distributed
|
|
113
|
+
as part of the Derivative Works; within the Source form or
|
|
114
|
+
documentation, if provided along with the Derivative Works; or,
|
|
115
|
+
within a display generated by the Derivative Works, if and
|
|
116
|
+
wherever such third-party notices normally appear. The contents
|
|
117
|
+
of the NOTICE file are for informational purposes only and
|
|
118
|
+
do not modify the License. You may add Your own attribution
|
|
119
|
+
notices within Derivative Works that You distribute, alongside
|
|
120
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
121
|
+
that such additional attribution notices cannot be construed
|
|
122
|
+
as modifying the License.
|
|
123
|
+
|
|
124
|
+
You may add Your own copyright statement to Your modifications and
|
|
125
|
+
may provide additional or different license terms and conditions
|
|
126
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
127
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
128
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
129
|
+
the conditions stated in this License.
|
|
130
|
+
|
|
131
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
132
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
133
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
134
|
+
this License, without any additional terms or conditions.
|
|
135
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
136
|
+
the terms of any separate license agreement you may have executed
|
|
137
|
+
with Licensor regarding such Contributions.
|
|
138
|
+
|
|
139
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
140
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
141
|
+
except as required for reasonable and customary use in describing the
|
|
142
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
143
|
+
|
|
144
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
145
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
146
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
147
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
148
|
+
implied, including, without limitation, any warranties or conditions
|
|
149
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
150
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
151
|
+
appropriateness of using or redistributing the Work and assume any
|
|
152
|
+
risks associated with Your exercise of permissions under this License.
|
|
153
|
+
|
|
154
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
155
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
156
|
+
unless required by applicable law (such as deliberate and grossly
|
|
157
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
158
|
+
liable to You for damages, including any direct, indirect, special,
|
|
159
|
+
incidental, or consequential damages of any character arising as a
|
|
160
|
+
result of this License or out of the use or inability to use the
|
|
161
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
162
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
163
|
+
other commercial damages or losses), even if such Contributor
|
|
164
|
+
has been advised of the possibility of such damages.
|
|
165
|
+
|
|
166
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
167
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
168
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
169
|
+
or other liability obligations and/or rights consistent with this
|
|
170
|
+
License. However, in accepting such obligations, You may act only
|
|
171
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
172
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
173
|
+
defend, and hold each Contributor harmless for any liability
|
|
174
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
175
|
+
of your accepting any such warranty or additional liability.
|
|
176
|
+
|
|
177
|
+
END OF TERMS AND CONDITIONS
|
|
178
|
+
|
|
179
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
180
|
+
|
|
181
|
+
To apply the Apache License to your work, attach the following
|
|
182
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
183
|
+
replaced with your own identifying information. (Don't include
|
|
184
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
185
|
+
comment syntax for the file format. We also recommend that a
|
|
186
|
+
file or class name and description of purpose be included on the
|
|
187
|
+
same "printed page" as the copyright notice for easier
|
|
188
|
+
identification within third-party archives.
|
|
189
|
+
|
|
190
|
+
Copyright 2026 spendgraph
|
|
191
|
+
|
|
192
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
193
|
+
you may not use this file except in compliance with the License.
|
|
194
|
+
You may obtain a copy of the License at
|
|
195
|
+
|
|
196
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
197
|
+
|
|
198
|
+
Unless required by applicable law or agreed to in writing, software
|
|
199
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
200
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
201
|
+
See the License for the specific language governing permissions and
|
|
202
|
+
limitations under the License.
|
package/README.md
CHANGED
|
@@ -1,243 +1,46 @@
|
|
|
1
1
|
# @spendgraph/prompt
|
|
2
2
|
|
|
3
|
-
Pull a stored prompt or
|
|
4
|
-
Talks to spendgraph through `@spendgraph/sdk` and to nothing else.
|
|
3
|
+
Pull a stored prompt or build one in code, render it, and record what it cost.
|
|
5
4
|
|
|
6
|
-
```
|
|
5
|
+
```bash
|
|
7
6
|
npm install @spendgraph/prompt
|
|
8
7
|
```
|
|
9
8
|
|
|
10
|
-
## Two ways in, one prompt
|
|
11
|
-
|
|
12
|
-
`pullPrompt` fetches the wording from the server. `buildCustomPrompt` declares
|
|
13
|
-
it in code. Both hand back the same `Prompt`, so `render`, `serialize` and
|
|
14
|
-
`call` behave identically and nothing downstream has to know which it was
|
|
15
|
-
given.
|
|
16
|
-
|
|
17
|
-
```ts
|
|
18
|
-
import { PromptClient, buildCustomPrompt } from "@spendgraph/prompt";
|
|
19
|
-
|
|
20
|
-
const prompts = new PromptClient({
|
|
21
|
-
apiKey: process.env.SPENDGRAPH_API_KEY,
|
|
22
|
-
baseUrl: "https://your-spendgraph.example.com",
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
const stored = await prompts.pull("billing-explainer");
|
|
26
|
-
const local = buildCustomPrompt({
|
|
27
|
-
name: "billing-explainer",
|
|
28
|
-
blocks: [{ title: "Role", body: "You explain invoices to {audience}." }],
|
|
29
|
-
question: "Explain this line item in {max_words} words.",
|
|
30
|
-
});
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
`pull` takes an id or a slug — the server resolves either. `pullPrompt(handle,
|
|
34
|
-
opts)` is the same thing without keeping a client around; it builds one per
|
|
35
|
-
call, so a service pulling repeatedly wants the class, which caches.
|
|
36
|
-
|
|
37
|
-
## Running one
|
|
38
|
-
|
|
39
|
-
Bind a model and the prompt runs itself:
|
|
40
|
-
|
|
41
9
|
```ts
|
|
42
|
-
import {
|
|
43
|
-
|
|
44
|
-
const llm = new Llm({ client: anthropic, model: "claude-opus-5" });
|
|
45
|
-
const chain = stored.bind(llm);
|
|
46
|
-
|
|
47
|
-
const answer = await chain.invoke({ audience: "students", max_words: 50 });
|
|
48
|
-
answer.output;
|
|
49
|
-
answer.rolloutId;
|
|
50
|
-
|
|
51
|
-
await chain.stream(values, { onText: (delta) => res.write(delta) });
|
|
52
|
-
const answers = await chain.batch(cases, { concurrency: 4 });
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
`bind` asks for a shape, not an import — anything with `call(messages)` returning
|
|
56
|
-
an outcome satisfies `Model`, and `Llm`'s reply already is one, so neither
|
|
57
|
-
package depends on the other.
|
|
58
|
-
|
|
59
|
-
### Or keep the call yourself
|
|
10
|
+
import { PromptClient } from "@spendgraph/prompt";
|
|
60
11
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
is handed to it rather than made by it.
|
|
12
|
+
const prompts = new PromptClient({ apiKey, baseUrl });
|
|
13
|
+
const prompt = await prompts.pull("billing-explainer");
|
|
64
14
|
|
|
65
|
-
|
|
66
|
-
const reply = await stored.call({ audience: "students", max_words: 50 }, async ({ messages }) => {
|
|
15
|
+
const reply = await prompt.call({ audience: "students" }, async ({ messages }) => {
|
|
67
16
|
const res = await anthropic.messages.create({ model, max_tokens: 400, messages });
|
|
68
17
|
return { output: res.content[0].text, model, inputTokens: 400, outputTokens: 12 };
|
|
69
18
|
});
|
|
70
19
|
```
|
|
71
20
|
|
|
72
|
-
|
|
73
|
-
text, model and tokens. Return the tokens or every cost on the prompt reads as
|
|
74
|
-
zero — the first call that forgets warns once.
|
|
75
|
-
|
|
76
|
-
Recording is on for every prompt and is fire-and-forget. There is no
|
|
77
|
-
client-level switch: pass `{ report: false }` on the call that should not be
|
|
78
|
-
recorded, which is where you know whether it should.
|
|
79
|
-
|
|
80
|
-
What was sent is built once and shared by the success path and the failure path,
|
|
81
|
-
so the two cannot disagree about it. That is the thing hand-written reporting
|
|
82
|
-
gets wrong. A callback that throws is recorded as failed and then rethrown: a
|
|
83
|
-
provider call that throws is otherwise recorded nowhere at all, which leaves the
|
|
84
|
-
run most in need of explaining with no evidence.
|
|
85
|
-
|
|
86
|
-
### What each kind records
|
|
87
|
-
|
|
88
|
-
A **server** prompt has a row and a version behind it, so a call writes a
|
|
89
|
-
rollout. A **custom** prompt has neither, and `POST /api/v1/prompts/:id/rollouts`
|
|
90
|
-
needs both — so it records usage instead. The tokens still land on the
|
|
91
|
-
dashboard, attributed to the model and tagged with the prompt's name; only the
|
|
92
|
-
wording goes unversioned. Read `prompt.source` to tell them apart.
|
|
93
|
-
|
|
94
|
-
A custom prompt is keyed on its name rather than a generated id, because that
|
|
95
|
-
name is what tags the usage event — a random one would file every run of the
|
|
96
|
-
same prompt under a different heading.
|
|
97
|
-
|
|
98
|
-
Leave `sdk` off `buildCustomPrompt` and the prompt touches the network never.
|
|
99
|
-
`{ report: false }` on any call runs the model and records nothing.
|
|
100
|
-
|
|
101
|
-
## Conversation history
|
|
102
|
-
|
|
103
|
-
Pass prior turns and they render between the system turn and the question,
|
|
104
|
-
oldest first.
|
|
105
|
-
|
|
106
|
-
```ts
|
|
107
|
-
const history = [
|
|
108
|
-
{ role: "user", content: "What is a proration?" },
|
|
109
|
-
{ role: "assistant", content: "A mid-cycle adjustment." },
|
|
110
|
-
];
|
|
111
|
-
|
|
112
|
-
prompt.format(values, { history });
|
|
113
|
-
await prompt.call(values, ({ messages }) => call(messages), { history });
|
|
114
|
-
```
|
|
115
|
-
|
|
116
|
-
History is passed through **verbatim** — it is a record of what was already
|
|
117
|
-
said, not a template, so `{placeholders}` inside it are left alone. Only the
|
|
118
|
-
prompt's own blocks and question are substituted. A `system` turn in the history
|
|
119
|
-
is dropped: the prompt owns that turn, and two of them is a different request
|
|
120
|
-
from the one the prompt describes.
|
|
121
|
-
|
|
122
|
-
Tools are still selected on the question, not on an older turn — the shortlist
|
|
123
|
-
has to serve what is being asked now.
|
|
124
|
-
|
|
125
|
-
A rollout records at most 50 messages, which is what the route accepts. A longer
|
|
126
|
-
conversation still runs; only its rollout is refused, and that refusal reaches
|
|
127
|
-
`onReportError` rather than your call — so the first time it happens this warns,
|
|
128
|
-
once, while somebody is still watching. `MAX_RECORDED_MESSAGES` is exported if
|
|
129
|
-
you want to trim before it bites.
|
|
130
|
-
|
|
131
|
-
## Fields
|
|
132
|
-
|
|
133
|
-
A `{placeholder}` is declared as a `FieldSpec` — a name, a type, whether it is
|
|
134
|
-
required, and how it renders. A missing required field throws before anything is
|
|
135
|
-
sent. Types come from `@spendgraph/sdk`, because a tool's arguments and a graph
|
|
136
|
-
node's inputs are the same problem.
|
|
137
|
-
|
|
138
|
-
```ts
|
|
139
|
-
prompt.format(values); // Message[], validated and substituted
|
|
140
|
-
prompt.serialize(values); // the field map that render used
|
|
141
|
-
```
|
|
142
|
-
|
|
143
|
-
## Caching, and invalidating it
|
|
144
|
-
|
|
145
|
-
`pull` serves a stale copy immediately and refreshes behind the caller, so an
|
|
146
|
-
edit takes effect within a TTL without any request paying for the fetch.
|
|
147
|
-
|
|
148
|
-
`promote` invalidates for you. Doing it by hand needs to drop **both** handles,
|
|
149
|
-
not just the one passed: `pull` keys on whatever string it was given, so the
|
|
150
|
-
same prompt sits under its uuid and its slug at once, and `promote` is only
|
|
151
|
-
reachable with a uuid while the dashboard teaches pulling by slug. Dropping one
|
|
152
|
-
left the other warm for a full TTL, still rendering the wording the promotion
|
|
153
|
-
replaced.
|
|
154
|
-
|
|
155
|
-
The delete runs even when nothing is stored under the handle: a cold isolate has
|
|
156
|
-
a fetch in flight and no entry yet, and the delete is what stops that in-flight
|
|
157
|
-
result being stored after the promotion.
|
|
158
|
-
|
|
159
|
-
```ts
|
|
160
|
-
await prompts.promote(promptId, versionId); // invalidates
|
|
161
|
-
prompts.invalidate(promptId); // both handles
|
|
162
|
-
prompts.invalidate(); // everything
|
|
163
|
-
```
|
|
164
|
-
|
|
165
|
-
`onPullError` is worth setting. Without it a background refresh that has stopped
|
|
166
|
-
resolving is silent, and a rename on the dashboard freezes a running service
|
|
167
|
-
with nothing anywhere saying so.
|
|
21
|
+
## What you get
|
|
168
22
|
|
|
169
|
-
|
|
23
|
+
- **Two ways in, one object.** `pullPrompt` fetches the wording from the
|
|
24
|
+
dashboard, `buildCustomPrompt` declares it in code, and everything downstream
|
|
25
|
+
behaves the same either way.
|
|
26
|
+
- **The model call stays yours.** This package never sees the provider response
|
|
27
|
+
— it renders, hands you the messages, and records what happened around them.
|
|
28
|
+
- **One record, two paths.** What was sent is built once and shared by the
|
|
29
|
+
success and failure paths, so the two cannot disagree.
|
|
30
|
+
- **A missing field throws before anything is sent**, not after you paid for the
|
|
31
|
+
call.
|
|
170
32
|
|
|
171
|
-
|
|
172
|
-
await prompts.versions(promptId, { origin: "user", limit: 20 });
|
|
173
|
-
await prompts.cases(promptId);
|
|
174
|
-
await prompts.setCases(promptId, cases);
|
|
175
|
-
```
|
|
176
|
-
|
|
177
|
-
`setCases` replaces the whole dataset. A dataset is a set — "which cases am I
|
|
178
|
-
evaluating against" has one answer at a time, and a merge would leave no way to
|
|
179
|
-
remove a case.
|
|
180
|
-
|
|
181
|
-
## Running server-side
|
|
182
|
-
|
|
183
|
-
`run`, `sample` and `runAll` execute the prompt on the server, where the spec,
|
|
184
|
-
rendering and pricing already live. This is the batch path. A production request
|
|
185
|
-
should pull, render and report instead: the server-side call adds a hop and makes
|
|
186
|
-
spendgraph a dependency of your uptime, which is the right trade for evaluation
|
|
187
|
-
and the wrong one for a user waiting on a response.
|
|
188
|
-
|
|
189
|
-
```ts
|
|
190
|
-
await prompts.run(promptId, values);
|
|
191
|
-
await prompts.sample(promptId, values, { k: 5 });
|
|
192
|
-
await prompts.runAll(promptId, values); // one rollout per saved model
|
|
193
|
-
```
|
|
194
|
-
|
|
195
|
-
`maxCostMicros` caps what those spend. Omit it and the client warns once that it
|
|
196
|
-
is unbounded; pass `null` to say you meant it.
|
|
197
|
-
|
|
198
|
-
## Tools
|
|
199
|
-
|
|
200
|
-
`call` and `invoke` accept anything that hands out a turn — `toolbus()` from
|
|
201
|
-
`@spendgraph/tools`, or your own. The callback gets the turn fully typed; the
|
|
202
|
-
rollout gets the tools offered and the steps taken.
|
|
203
|
-
|
|
204
|
-
```ts
|
|
205
|
-
await prompt.call(values, ({ messages, turn }) => call(messages, turn), { tools: bus });
|
|
206
|
-
```
|
|
207
|
-
|
|
208
|
-
Which tools were offered is recorded as well as which were called: without it a
|
|
209
|
-
rollout cannot tell "chose not to" from "was never offered", which are opposite
|
|
210
|
-
bugs.
|
|
211
|
-
|
|
212
|
-
## What is exported
|
|
213
|
-
|
|
214
|
-
The main entry is six values, because everything else is already wired into
|
|
215
|
-
`PromptClient`:
|
|
216
|
-
|
|
217
|
-
```ts
|
|
218
|
-
import {
|
|
219
|
-
PromptClient, // pull, run, report, versions, datasets
|
|
220
|
-
pullPrompt, // one prompt, without keeping a client
|
|
221
|
-
buildCustomPrompt, // a prompt written in code
|
|
222
|
-
BudgetExceededError,
|
|
223
|
-
FieldValidationError,
|
|
224
|
-
MAX_RECORDED_MESSAGES,
|
|
225
|
-
} from "@spendgraph/prompt";
|
|
226
|
-
```
|
|
227
|
-
|
|
228
|
-
Everything a prompt can do hangs off the object itself — `format`, `serialize`,
|
|
229
|
-
`call`, `bind` — so there is nothing else to import to use one.
|
|
230
|
-
|
|
231
|
-
Two subpaths hold the rest:
|
|
33
|
+
## Docs
|
|
232
34
|
|
|
233
35
|
| | |
|
|
234
36
|
| --- | --- |
|
|
235
|
-
|
|
|
236
|
-
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
37
|
+
| [Overview](https://spendgraph.locusgraph.com/docs/prompt/overview) | Two ways in, one prompt |
|
|
38
|
+
| [Fields](https://spendgraph.locusgraph.com/docs/prompt/fields) | Placeholders, types, and history |
|
|
39
|
+
| [Calling a prompt](https://spendgraph.locusgraph.com/docs/prompt/calling) | call, bind, tools, and what is recorded |
|
|
40
|
+
| [Caching](https://spendgraph.locusgraph.com/docs/prompt/caching) | Stale-while-refresh, and both handles |
|
|
41
|
+
| [Running server-side](https://spendgraph.locusgraph.com/docs/prompt/running) | run, sample, runAll, and the ceiling |
|
|
42
|
+
| [Versions and datasets](https://spendgraph.locusgraph.com/docs/prompt/datasets) | The split the server decides, and why |
|
|
240
43
|
|
|
241
44
|
## License
|
|
242
45
|
|
|
243
|
-
|
|
46
|
+
Apache-2.0
|
package/dist/budget/budget.d.ts
CHANGED
|
@@ -14,6 +14,16 @@ export declare class Budget {
|
|
|
14
14
|
* `undefined` means no ceiling and warns once — an unbounded client in a
|
|
15
15
|
* scheduled script is how a surprise bill happens. `null` means the same
|
|
16
16
|
* thing deliberately, and says nothing.
|
|
17
|
+
*
|
|
18
|
+
* A ceiling that is `NaN` or negative is refused rather than accepted.
|
|
19
|
+
* `NaN >= NaN` is false, so a `NaN` limit never fires and the guard against an
|
|
20
|
+
* expensive accident silently is not there — and
|
|
21
|
+
* `Number(process.env.MAX_COST_MICROS)` on an unset variable is `NaN`, which
|
|
22
|
+
* is how it arrives without anyone writing it. A negative ceiling is refused
|
|
23
|
+
* because it can only be a mistake, and expressing that mistake as "every
|
|
24
|
+
* request is refused" sends someone looking in the wrong place.
|
|
25
|
+
*
|
|
26
|
+
* `0` is kept: spending nothing is a thing a caller can mean.
|
|
17
27
|
*/
|
|
18
28
|
constructor(maxCostMicros?: number | null);
|
|
19
29
|
spent(): number;
|
package/dist/budget/budget.js
CHANGED
|
@@ -1,31 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
let warned = false;
|
|
3
|
-
export class Budget {
|
|
4
|
-
used = 0;
|
|
5
|
-
limit;
|
|
6
|
-
constructor(maxCostMicros) {
|
|
7
|
-
this.limit = maxCostMicros ?? Infinity;
|
|
8
|
-
if (maxCostMicros === undefined && !warned) {
|
|
9
|
-
warned = true;
|
|
10
|
-
console.warn("[prompt] No maxCostMicros set — this client can spend without limit. " +
|
|
11
|
-
"Pass maxCostMicros, or null to say you meant it.");
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
spent() {
|
|
15
|
-
return this.used;
|
|
16
|
-
}
|
|
17
|
-
remaining() {
|
|
18
|
-
return this.limit === Infinity ? Infinity : Math.max(0, this.limit - this.used);
|
|
19
|
-
}
|
|
20
|
-
add(costMicros) {
|
|
21
|
-
if (Number.isFinite(costMicros) && costMicros > 0)
|
|
22
|
-
this.used += costMicros;
|
|
23
|
-
}
|
|
24
|
-
assertAffordable() {
|
|
25
|
-
if (this.used >= this.limit)
|
|
26
|
-
throw new BudgetExceededError(this.used, this.limit);
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
export function resetBudgetWarning() {
|
|
30
|
-
warned = false;
|
|
31
|
-
}
|
|
1
|
+
import{BudgetExceededError as t}from"./errors.js";let i=!1;class s{used=0;limit;constructor(e){if(typeof e=="number"&&e<0)throw new Error(`maxCostMicros is ${e}. A ceiling below zero refuses every request, which is rarely what was meant. Pass 0 to spend nothing, or null for no limit.`);if(typeof e=="number"&&!(Number.isFinite(e)||e===Number.POSITIVE_INFINITY))throw new Error(`maxCostMicros is ${e}, which is not a ceiling \u2014 nothing would ever reach it. Pass a number of micro-USD, or null for no limit.`);this.limit=e??1/0,e===void 0&&!i&&(i=!0,console.warn("[prompt] No maxCostMicros set \u2014 this client can spend without limit. Pass maxCostMicros, or null to say you meant it."))}spent(){return this.used}remaining(){return this.limit===1/0?1/0:Math.max(0,this.limit-this.used)}add(e){Number.isFinite(e)&&e>0&&(this.used+=e)}assertAffordable(){if(this.used>=this.limit)throw new t(this.used,this.limit)}}function u(){i=!1}export{s as Budget,u as resetBudgetWarning};
|
package/dist/budget/errors.js
CHANGED
|
@@ -1,11 +1 @@
|
|
|
1
|
-
|
|
2
|
-
spentMicros;
|
|
3
|
-
limitMicros;
|
|
4
|
-
constructor(spentMicros, limitMicros) {
|
|
5
|
-
super(`Cost ceiling reached: spent $${(spentMicros / 1e6).toFixed(4)} of ` +
|
|
6
|
-
`$${(limitMicros / 1e6).toFixed(4)}. Raise maxCostMicros or start a new client.`);
|
|
7
|
-
this.name = "BudgetExceededError";
|
|
8
|
-
this.spentMicros = spentMicros;
|
|
9
|
-
this.limitMicros = limitMicros;
|
|
10
|
-
}
|
|
11
|
-
}
|
|
1
|
+
class s extends Error{spentMicros;limitMicros;constructor(e,r){super(`Cost ceiling reached: spent $${(e/1e6).toFixed(4)} of $${(r/1e6).toFixed(4)}. Raise maxCostMicros or start a new client.`),this.name="BudgetExceededError",this.spentMicros=e,this.limitMicros=r}}export{s as BudgetExceededError};
|
package/dist/budget/index.js
CHANGED
|
@@ -1,2 +1 @@
|
|
|
1
|
-
|
|
2
|
-
export { BudgetExceededError } from "./errors.js";
|
|
1
|
+
import{Budget as t,resetBudgetWarning as d}from"./budget.js";import{BudgetExceededError as g}from"./errors.js";export{t as Budget,g as BudgetExceededError,d as resetBudgetWarning};
|
package/dist/build/build.d.ts
CHANGED
|
@@ -7,12 +7,21 @@ export interface BuildOptions {
|
|
|
7
7
|
metadata?: Record<string, string | number | boolean>;
|
|
8
8
|
/** Replaces the once-only warning about a callback that returned no tokens. */
|
|
9
9
|
onEmptyTokens?: (entry: ReportInput) => void;
|
|
10
|
+
/**
|
|
11
|
+
* Called when sending the usage fails.
|
|
12
|
+
*
|
|
13
|
+
* The send is fire-and-forget so a telemetry outage cannot break the call it
|
|
14
|
+
* measures, which leaves nothing to catch it — and what is lost is spend
|
|
15
|
+
* data, which nothing downstream can reconstruct. `PromptClient` has the same
|
|
16
|
+
* option for rollouts and passes it down to prompts it builds.
|
|
17
|
+
*/
|
|
18
|
+
onReportError?: (err: unknown) => void;
|
|
10
19
|
}
|
|
11
20
|
/**
|
|
12
21
|
* A prompt written in code rather than pulled from the server.
|
|
13
22
|
*
|
|
14
|
-
* The same `Prompt` `pullPrompt` returns — same `
|
|
15
|
-
* same `
|
|
23
|
+
* The same `Prompt` `pullPrompt` returns — same `format`, same `serialize`,
|
|
24
|
+
* same `call` — so nothing downstream has to know which one it was handed.
|
|
16
25
|
* Give it an `sdk` and it records what the call cost; leave it off and it
|
|
17
26
|
* touches the network never.
|
|
18
27
|
*/
|
package/dist/build/build.js
CHANGED
|
@@ -1,57 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { applyVariables } from "../render/index.js";
|
|
3
|
-
function neverBreakTheCallItMeasured() { }
|
|
4
|
-
let warnedAboutTokens = false;
|
|
5
|
-
function warnOnEmptyTokens(entry) {
|
|
6
|
-
if (warnedAboutTokens)
|
|
7
|
-
return;
|
|
8
|
-
if ((entry.inputTokens ?? 0) > 0 || (entry.outputTokens ?? 0) > 0)
|
|
9
|
-
return;
|
|
10
|
-
warnedAboutTokens = true;
|
|
11
|
-
console.warn("[prompt] trace() recorded a call with no tokens. This package never sees " +
|
|
12
|
-
"the provider response, so return inputTokens and outputTokens from your callback " +
|
|
13
|
-
"or every cost on this prompt reads as zero.");
|
|
14
|
-
}
|
|
15
|
-
function usageRecorder(sdk, name, opts) {
|
|
16
|
-
const onEmpty = opts.onEmptyTokens ?? warnOnEmptyTokens;
|
|
17
|
-
return {
|
|
18
|
-
enabled: true,
|
|
19
|
-
write(_promptId, entry) {
|
|
20
|
-
if (entry.status === "failed")
|
|
21
|
-
return;
|
|
22
|
-
onEmpty(entry);
|
|
23
|
-
void sdk.ingest
|
|
24
|
-
.send([
|
|
25
|
-
{
|
|
26
|
-
eventId: entry.rolloutId,
|
|
27
|
-
model: entry.model,
|
|
28
|
-
inputTokens: entry.inputTokens ?? 0,
|
|
29
|
-
outputTokens: entry.outputTokens ?? 0,
|
|
30
|
-
cacheReadTokens: entry.cacheReadTokens,
|
|
31
|
-
cacheWriteTokens: entry.cacheWriteTokens,
|
|
32
|
-
citationTokens: entry.citationTokens,
|
|
33
|
-
reasoningTokens: entry.reasoningTokens,
|
|
34
|
-
metadata: { ...opts.metadata, prompt: name },
|
|
35
|
-
},
|
|
36
|
-
])
|
|
37
|
-
.catch(neverBreakTheCallItMeasured);
|
|
38
|
-
},
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
export function buildCustomPrompt(spec, opts = {}) {
|
|
42
|
-
const variables = spec.variables ?? {};
|
|
43
|
-
const blocks = (spec.blocks ?? []).map((b) => ({
|
|
44
|
-
title: applyVariables(b.title, variables),
|
|
45
|
-
body: applyVariables(b.body, variables),
|
|
46
|
-
}));
|
|
47
|
-
return makePrompt({
|
|
48
|
-
id: spec.name,
|
|
49
|
-
name: spec.name,
|
|
50
|
-
slug: null,
|
|
51
|
-
blocks,
|
|
52
|
-
question: applyVariables(spec.question ?? "", variables),
|
|
53
|
-
fields: spec.fields ?? [],
|
|
54
|
-
versionId: null,
|
|
55
|
-
models: spec.models ?? [],
|
|
56
|
-
}, "custom", opts.sdk ? usageRecorder(opts.sdk, spec.name, opts) : noRecorder);
|
|
57
|
-
}
|
|
1
|
+
import{makePrompt as l,noRecorder as d}from"../prompt/index.js";import{applyVariables as r}from"../render/index.js";let u=!1;function m(o){u||(o.inputTokens??0)>0||(o.outputTokens??0)>0||(u=!0,console.warn("[prompt] call() recorded a call with no tokens. This package never sees the provider response, so return inputTokens and outputTokens from your callback or every cost on this prompt reads as zero."))}function c(o,t,n){const a=n.onEmptyTokens??m,s=i=>{try{n.onReportError?.(i)}catch{}};return{enabled:!0,write(i,e){e.status!=="failed"&&(a(e),o.ingest.send([{eventId:e.rolloutId,model:e.model,inputTokens:e.inputTokens??0,outputTokens:e.outputTokens??0,cacheReadTokens:e.cacheReadTokens,cacheWriteTokens:e.cacheWriteTokens,citationTokens:e.citationTokens,reasoningTokens:e.reasoningTokens,metadata:{...n.metadata,prompt:t}}]).catch(s))}}}function T(o,t={}){const n=o.variables??{},a=(o.blocks??[]).map(s=>({title:r(s.title,n),body:r(s.body,n)}));return l({id:o.name,name:o.name,slug:null,blocks:a,question:r(o.question??"",n),fields:o.fields??[],versionId:null,models:o.models??[]},"custom",t.sdk?c(t.sdk,o.name,t):d)}export{T as buildCustomPrompt};
|
package/dist/build/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
import{buildCustomPrompt as r}from"./build.js";export{r as buildCustomPrompt};
|