@spendgraph/prompt 0.2.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 (72) hide show
  1. package/README.md +240 -0
  2. package/dist/budget/budget.d.ts +31 -0
  3. package/dist/budget/budget.js +53 -0
  4. package/dist/budget/errors.d.ts +12 -0
  5. package/dist/budget/errors.js +18 -0
  6. package/dist/budget/index.d.ts +2 -0
  7. package/dist/budget/index.js +2 -0
  8. package/dist/build/build.d.ts +19 -0
  9. package/dist/build/build.js +73 -0
  10. package/dist/build/index.d.ts +2 -0
  11. package/dist/build/index.js +1 -0
  12. package/dist/cache/cache.d.ts +118 -0
  13. package/dist/cache/cache.js +198 -0
  14. package/dist/cache/index.d.ts +1 -0
  15. package/dist/cache/index.js +1 -0
  16. package/dist/client.d.ts +147 -0
  17. package/dist/client.js +230 -0
  18. package/dist/index.d.ts +11 -0
  19. package/dist/index.js +5 -0
  20. package/dist/internals.d.ts +17 -0
  21. package/dist/internals.js +13 -0
  22. package/dist/prompt/bind.d.ts +60 -0
  23. package/dist/prompt/bind.js +21 -0
  24. package/dist/prompt/factory.d.ts +49 -0
  25. package/dist/prompt/factory.js +118 -0
  26. package/dist/prompt/index.d.ts +4 -0
  27. package/dist/prompt/index.js +2 -0
  28. package/dist/pull/index.d.ts +1 -0
  29. package/dist/pull/index.js +1 -0
  30. package/dist/pull/pull.d.ts +12 -0
  31. package/dist/pull/pull.js +31 -0
  32. package/dist/render/index.d.ts +4 -0
  33. package/dist/render/index.js +3 -0
  34. package/dist/render/messages.d.ts +12 -0
  35. package/dist/render/messages.js +24 -0
  36. package/dist/render/system.d.ts +8 -0
  37. package/dist/render/system.js +19 -0
  38. package/dist/render/types.d.ts +15 -0
  39. package/dist/render/types.js +1 -0
  40. package/dist/render/variables.d.ts +11 -0
  41. package/dist/render/variables.js +29 -0
  42. package/dist/run/across-models.d.ts +7 -0
  43. package/dist/run/across-models.js +6 -0
  44. package/dist/run/concurrency.d.ts +7 -0
  45. package/dist/run/concurrency.js +18 -0
  46. package/dist/run/id.d.ts +2 -0
  47. package/dist/run/id.js +4 -0
  48. package/dist/run/index.d.ts +5 -0
  49. package/dist/run/index.js +5 -0
  50. package/dist/run/once.d.ts +4 -0
  51. package/dist/run/once.js +22 -0
  52. package/dist/run/sample.d.ts +17 -0
  53. package/dist/run/sample.js +39 -0
  54. package/dist/types/dataset.d.ts +22 -0
  55. package/dist/types/dataset.js +1 -0
  56. package/dist/types/index.d.ts +8 -0
  57. package/dist/types/index.js +1 -0
  58. package/dist/types/payload.d.ts +16 -0
  59. package/dist/types/payload.js +1 -0
  60. package/dist/types/prompt.d.ts +73 -0
  61. package/dist/types/prompt.js +1 -0
  62. package/dist/types/result.d.ts +13 -0
  63. package/dist/types/result.js +1 -0
  64. package/dist/types/rollout.d.ts +8 -0
  65. package/dist/types/rollout.js +1 -0
  66. package/dist/types/run.d.ts +18 -0
  67. package/dist/types/run.js +1 -0
  68. package/dist/types/trace.d.ts +71 -0
  69. package/dist/types/trace.js +1 -0
  70. package/dist/types/version.d.ts +21 -0
  71. package/dist/types/version.js +1 -0
  72. package/package.json +63 -0
@@ -0,0 +1,198 @@
1
+ export class PullCache {
2
+ store = new Map();
3
+ ttlMs;
4
+ maxSize;
5
+ now;
6
+ /** Keys with a refresh in flight, so N concurrent readers cause one fetch. */
7
+ refreshing = new Set();
8
+ /** When a key's refresh last failed, so a dead handle is not retried per call. */
9
+ failures = new Map();
10
+ retryAfterMs;
11
+ onRefreshError;
12
+ identify;
13
+ /**
14
+ * A monotonic counter and the tick at which each name was last invalidated.
15
+ *
16
+ * A fetch records the counter when it starts and, when it lands, asks whether
17
+ * anything it turned out to be — its key, its id, its slug — has been
18
+ * invalidated since. That is the whole protocol, and it holds for the cold
19
+ * path, the background refresh, and a handle nobody has seen before, none of
20
+ * which the previous per-key epoch could cover.
21
+ */
22
+ tick = 0;
23
+ invalidatedAt = new Map();
24
+ /**
25
+ * The tick of the last `clear()`.
26
+ *
27
+ * `clear` cannot name a fetch it has never seen — a cold one is in neither
28
+ * `store` nor `refreshing` — so it raises a floor instead: anything that
29
+ * started before it is out of date whatever it turns out to be.
30
+ */
31
+ clearedAt = -1;
32
+ /** Fetches outstanding, so the log above can be dropped when none remain. */
33
+ outstanding = 0;
34
+ constructor(opts = {}) {
35
+ // A TTL of 0 disables caching without a separate flag; only undefined
36
+ // falls back to the default.
37
+ this.ttlMs = (opts.ttlSeconds ?? 300) * 1000;
38
+ this.maxSize = Math.max(1, opts.maxSize ?? 100);
39
+ this.now = opts.now ?? (() => Date.now());
40
+ this.retryAfterMs = (opts.retryAfterSeconds ?? 60) * 1000;
41
+ this.onRefreshError = opts.onRefreshError;
42
+ this.identify = opts.identify;
43
+ }
44
+ get(key) {
45
+ const entry = this.store.get(key);
46
+ if (!entry)
47
+ return undefined;
48
+ // Map iterates in insertion order, so re-setting marks most-recently-used.
49
+ this.store.delete(key);
50
+ this.store.set(key, entry);
51
+ const ageMs = this.now() - entry.at;
52
+ return { value: entry.value, stale: ageMs > this.ttlMs, ageMs };
53
+ }
54
+ set(key, value) {
55
+ this.store.delete(key);
56
+ this.store.set(key, { value, at: this.now() });
57
+ while (this.store.size > this.maxSize) {
58
+ const oldest = this.store.keys().next().value;
59
+ if (oldest === undefined)
60
+ break;
61
+ this.store.delete(oldest);
62
+ // The bookkeeping goes with the entry unless a fetch for it is still
63
+ // running, otherwise these grow forever in a class whose contract is a
64
+ // bounded LRU — one permanent entry per distinct key ever seen.
65
+ if (!this.refreshing.has(oldest))
66
+ this.failures.delete(oldest);
67
+ }
68
+ }
69
+ /**
70
+ * Runs `refresh` unless one is already in flight for this key.
71
+ *
72
+ * Without the guard, a burst of requests arriving just after the TTL lapses
73
+ * each start their own refresh — the stampede the cache exists to prevent.
74
+ * Failures are swallowed on purpose: a background refresh that throws must not
75
+ * surface in a request that was already answered from cache.
76
+ */
77
+ refreshInBackground(key, refresh) {
78
+ if (this.refreshing.has(key))
79
+ return;
80
+ // Backed off after a failure. A handle that stopped resolving — routine
81
+ // since a rename re-mints the slug — would otherwise fire a doomed request
82
+ // on every pull, forever. The entry stays; the stampede stops.
83
+ const failedAt = this.failures.get(key);
84
+ if (failedAt !== undefined && this.now() - failedAt < this.retryAfterMs)
85
+ return;
86
+ this.refreshing.add(key);
87
+ const startedAt = this.beginFetch();
88
+ void refresh()
89
+ .then((value) => this.settle(key, value, startedAt))
90
+ .catch((err) => {
91
+ this.abandon(key);
92
+ // Surfaced rather than swallowed. `report` has onReportError for exactly
93
+ // this reason; without a counterpart here a service can serve a frozen
94
+ // prompt indefinitely with nothing anywhere saying so.
95
+ //
96
+ // Wrapped, because nobody is holding this promise: `pullWithCache` has
97
+ // already returned the stale value, so a callback that throws would be
98
+ // an unhandled rejection — process exit in Node, isolate abort in a
99
+ // Worker.
100
+ try {
101
+ this.onRefreshError?.(err, key);
102
+ }
103
+ catch {
104
+ /* a reporter that cannot report is not worth an outage */
105
+ }
106
+ })
107
+ .finally(() => {
108
+ this.refreshing.delete(key);
109
+ this.forgetLogIfIdle();
110
+ });
111
+ }
112
+ /** Records a fetch starting, and the tick it must be judged against. */
113
+ beginFetch() {
114
+ this.outstanding++;
115
+ return this.tick;
116
+ }
117
+ /** Stores a landed fetch unless what it fetched was invalidated meanwhile. */
118
+ settle(key, value, startedAt) {
119
+ this.outstanding--;
120
+ const names = [key, ...(this.identify?.(value) ?? [])];
121
+ const overtaken = startedAt < this.clearedAt ||
122
+ names.some((n) => (this.invalidatedAt.get(n) ?? -1) > startedAt);
123
+ if (!overtaken) {
124
+ // Only once the value is kept: clearing first meant a fetch that lost the
125
+ // race still wiped a live backoff, turning bounded retry into a retry on
126
+ // every call.
127
+ this.failures.delete(key);
128
+ this.set(key, value);
129
+ }
130
+ this.forgetLogIfIdle();
131
+ }
132
+ /** Ends a fetch that threw, and backs the handle off. */
133
+ abandon(key) {
134
+ this.outstanding--;
135
+ // Recorded on the cold path too — the one a fresh isolate takes — or a
136
+ // permanently 404ing handle fires one upstream request per pull, forever.
137
+ this.failures.set(key, this.now());
138
+ this.boundFailures();
139
+ this.forgetLogIfIdle();
140
+ }
141
+ /** Records that everything under this name is out of date. */
142
+ invalidate(name) {
143
+ this.invalidatedAt.set(name, ++this.tick);
144
+ }
145
+ /** The log only matters while a fetch could still be judged against it. */
146
+ forgetLogIfIdle() {
147
+ if (this.outstanding === 0 && this.refreshing.size === 0)
148
+ this.invalidatedAt.clear();
149
+ }
150
+ /** Keeps `failures` inside the same bound as the entries themselves. */
151
+ boundFailures() {
152
+ while (this.failures.size > this.maxSize) {
153
+ const oldest = this.failures.keys().next().value;
154
+ if (oldest === undefined)
155
+ break;
156
+ this.failures.delete(oldest);
157
+ }
158
+ }
159
+ /** Every key whose entry satisfies `match`. */
160
+ keysWhere(match) {
161
+ const out = [];
162
+ for (const [key, entry] of this.store)
163
+ if (match(entry.value))
164
+ out.push(key);
165
+ return out;
166
+ }
167
+ delete(key) {
168
+ this.store.delete(key);
169
+ // The backoff goes with it, or a handle that has demonstrably recovered
170
+ // keeps skipping its refresh for the whole retry window.
171
+ this.failures.delete(key);
172
+ this.invalidate(key);
173
+ // Nothing outstanding means nothing to judge against it — residue that
174
+ // grew by one per prompt ever invalidated.
175
+ this.forgetLogIfIdle();
176
+ }
177
+ clear() {
178
+ // Every key the cache has an opinion about, not just stored ones: a cold
179
+ // fetch is in flight under a key not in `store` yet.
180
+ this.clearedAt = ++this.tick;
181
+ this.store.clear();
182
+ this.failures.clear();
183
+ this.forgetLogIfIdle();
184
+ }
185
+ get size() {
186
+ return this.store.size;
187
+ }
188
+ /**
189
+ * How many keys the cache holds bookkeeping for, entries aside.
190
+ *
191
+ * Exposed for the test that this stays bounded: `epochs` and `failures` are
192
+ * private and grow on paths `size` cannot see, so a test written against
193
+ * `size` passes whether or not they leak — which is what the first one did.
194
+ */
195
+ get trackedKeys() {
196
+ return new Set([...this.invalidatedAt.keys(), ...this.failures.keys()]).size;
197
+ }
198
+ }
@@ -0,0 +1 @@
1
+ export { type CacheHit, type CacheOptions, PullCache } from "./cache.js";
@@ -0,0 +1 @@
1
+ export { PullCache } from "./cache.js";
@@ -0,0 +1,147 @@
1
+ import { type ClientOptions, Spendgraph } from "@spendgraph/sdk";
2
+ import { type BuildOptions } from "./build/index.js";
3
+ import { type CacheOptions } from "./cache/index.js";
4
+ import type { DatasetCase, DatasetSummary, Prompt, PromptPayload, PromptSpec, PromptVersion, ReportInput, RunOptions, RunResult } from "./types/index.js";
5
+ export interface PromptClientOptions extends Omit<ClientOptions, "apiKey" | "baseUrl"> {
6
+ /** spendgraph API key (sg_…). Without it, `pull` throws and `report` no-ops. */
7
+ apiKey: string | undefined;
8
+ baseUrl: string;
9
+ cache?: CacheOptions<PromptPayload>;
10
+ /** Max requests in flight for sample() and runAll(). Default 4. */
11
+ concurrency?: number;
12
+ /**
13
+ * Stop spending past this, in micro-dollars. Omit and the client warns once
14
+ * that it is unbounded; pass `null` to say you meant it.
15
+ */
16
+ maxCostMicros?: number | null;
17
+ onReportError?: (err: unknown) => void;
18
+ onPullError?: (err: unknown, key: string) => void;
19
+ }
20
+ /**
21
+ * Stored prompts, and the spend they account for.
22
+ *
23
+ * Everything that leaves the process goes through `@spendgraph/sdk`; this class
24
+ * owns the caching, the budget and the rollout bookkeeping on top of it.
25
+ */
26
+ export declare class PromptClient {
27
+ /** The SDK underneath. Reach for it for anything this class does not wrap. */
28
+ readonly sdk: Spendgraph;
29
+ private readonly cache;
30
+ private readonly hasKey;
31
+ private readonly onReportError?;
32
+ private readonly concurrency;
33
+ private readonly budget;
34
+ private warnedAboutTokens;
35
+ constructor(opts: PromptClientOptions);
36
+ private recorder;
37
+ /**
38
+ * A prompt, from cache when one is warm.
39
+ *
40
+ * Serves a stale copy immediately and refreshes behind the caller, so an edit
41
+ * takes effect within a TTL without any request ever paying for the fetch.
42
+ *
43
+ * Takes an id or a slug — the server resolves either. Cached under whatever
44
+ * you passed, so pulling the same prompt both ways keeps two entries.
45
+ */
46
+ pull(handle: string): Promise<Prompt>;
47
+ /** A prompt written in code, wired to record its usage through this client. */
48
+ build(spec: PromptSpec, opts?: Omit<BuildOptions, "sdk">): Prompt;
49
+ /**
50
+ * Said once, because this package cannot count tokens it never saw.
51
+ *
52
+ * A callback that forgets to return them records a rollout reading zero in
53
+ * and zero out, which prices at nothing and quietly drags every average that
54
+ * includes it. Silence here is how that goes unnoticed for a month.
55
+ */
56
+ private warnOnEmptyTokens;
57
+ /**
58
+ * Runs the prompt server-side and records the rollout.
59
+ *
60
+ * The batch path. A production request should pull, render and report
61
+ * instead — this adds a hop and makes spendgraph a dependency of the caller's
62
+ * uptime, which is the right trade for evaluation and the wrong one for a
63
+ * user waiting on a response.
64
+ */
65
+ run(promptId: string, values?: Record<string, unknown>, opts?: RunOptions): Promise<RunResult>;
66
+ /** k repetitions of one case. Always k results, failures included. */
67
+ sample(promptId: string, values?: Record<string, unknown>, opts?: RunOptions & {
68
+ k?: number;
69
+ concurrency?: number;
70
+ }): Promise<RunResult[]>;
71
+ /** One rollout per model — the playground's comparison, headless. */
72
+ runAll(promptId: string, values?: Record<string, unknown>, opts?: RunOptions & {
73
+ models?: string[];
74
+ concurrency?: number;
75
+ }): Promise<RunResult[]>;
76
+ /** The dataset a prompt is evaluated against, with its split counts. */
77
+ cases(promptId: string): Promise<DatasetSummary>;
78
+ /**
79
+ * Replaces the whole dataset.
80
+ *
81
+ * A dataset is a set: "which cases am I evaluating against" has one answer at
82
+ * a time, and a merge would leave no way to remove a case or to know from the
83
+ * outside what the set currently is.
84
+ */
85
+ setCases(promptId: string, cases: DatasetCase[]): Promise<Omit<DatasetSummary, "cases">>;
86
+ /** Micro-dollars this client has spent on runs it initiated. */
87
+ spent(): number;
88
+ /** What is left of the ceiling, or Infinity when none was set. */
89
+ remaining(): number;
90
+ /** Every wording this prompt has had, newest first. */
91
+ versions(promptId: string, opts?: {
92
+ origin?: "user" | "assay";
93
+ limit?: number;
94
+ }): Promise<{
95
+ versions: PromptVersion[];
96
+ currentVersionId: string | null;
97
+ }>;
98
+ /**
99
+ * Serves this version from now on.
100
+ *
101
+ * Invalidates the cache: a promotion that left a warm entry in place would
102
+ * keep sending the old wording for up to a TTL, which is the one moment a
103
+ * caller is actively watching for the change.
104
+ */
105
+ promote(promptId: string, versionId: string): Promise<{
106
+ promoted: string;
107
+ previous?: string | null;
108
+ unchanged?: boolean;
109
+ }>;
110
+ /**
111
+ * Drops whatever is cached, so the next pull refetches.
112
+ *
113
+ * Both handles, not just the one passed. `pull` keys the cache on whatever
114
+ * string it was given, so the same prompt can sit under its uuid and its
115
+ * slug at once — and `promote` is only reachable with a uuid while the
116
+ * dashboard teaches pulling by slug.
117
+ */
118
+ invalidate(promptId?: string): void;
119
+ /**
120
+ * Records a rollout the caller executed.
121
+ *
122
+ * Never throws and never rejects. This sits beside a user-facing request that
123
+ * has already been answered; telemetry that can break the thing it measures
124
+ * is worse than no telemetry.
125
+ */
126
+ /**
127
+ * Records a rollout and returns what the server priced it at, in micro-USD,
128
+ * or undefined where the model had no price to compute one from.
129
+ *
130
+ * The price is computed there, from the tokens and the project's pricing
131
+ * table, so this is the only place it exists — and it is why `report` cannot
132
+ * simply be reused: that one answers with the rollout id.
133
+ *
134
+ * `priced: false` is answered as undefined rather than as the stored zero.
135
+ * The column is `notNull`, so an unpriced rollout is written as zero and the
136
+ * flag beside it is the only thing that tells free apart from unknown.
137
+ */
138
+ private priced;
139
+ report(promptId: string, input: ReportInput): Promise<string>;
140
+ }
141
+ /**
142
+ * One prompt from the server, without keeping a client around.
143
+ *
144
+ * Convenience over `new PromptClient(...).pull(...)`, and it builds a client
145
+ * per call — a service pulling repeatedly wants the class, which caches.
146
+ */
147
+ export declare function pullPrompt(handle: string, opts: PromptClientOptions): Promise<Prompt>;
package/dist/client.js ADDED
@@ -0,0 +1,230 @@
1
+ import { Spendgraph } from "@spendgraph/sdk";
2
+ import { Budget } from "./budget/index.js";
3
+ import { buildCustomPrompt } from "./build/index.js";
4
+ import { PullCache } from "./cache/index.js";
5
+ import { makePrompt } from "./prompt/index.js";
6
+ import { pullWithCache } from "./pull/index.js";
7
+ import { newRolloutId } from "./run/id.js";
8
+ import { runAcrossModels, runOnce, sampleRuns } from "./run/index.js";
9
+ const DEFAULT_CONCURRENCY = 4;
10
+ /**
11
+ * Stored prompts, and the spend they account for.
12
+ *
13
+ * Everything that leaves the process goes through `@spendgraph/sdk`; this class
14
+ * owns the caching, the budget and the rollout bookkeeping on top of it.
15
+ */
16
+ export class PromptClient {
17
+ /** The SDK underneath. Reach for it for anything this class does not wrap. */
18
+ sdk;
19
+ cache;
20
+ hasKey;
21
+ onReportError;
22
+ concurrency;
23
+ budget;
24
+ warnedAboutTokens = false;
25
+ constructor(opts) {
26
+ const { cache, onReportError, onPullError, concurrency, maxCostMicros, ...clientOpts } = opts;
27
+ this.sdk = new Spendgraph(clientOpts);
28
+ this.cache = new PullCache({
29
+ ...cache,
30
+ onRefreshError: cache?.onRefreshError ?? onPullError,
31
+ identify: (p) => [p.id, p.slug].filter((v) => Boolean(v)),
32
+ });
33
+ this.hasKey = Boolean(opts.apiKey);
34
+ this.onReportError = onReportError;
35
+ this.concurrency = Math.max(1, concurrency ?? DEFAULT_CONCURRENCY);
36
+ this.budget = new Budget("maxCostMicros" in opts ? maxCostMicros : undefined);
37
+ }
38
+ recorder() {
39
+ return {
40
+ enabled: true,
41
+ write: (promptId, entry) => {
42
+ this.warnOnEmptyTokens(entry);
43
+ return this.priced(promptId, entry);
44
+ },
45
+ };
46
+ }
47
+ /**
48
+ * A prompt, from cache when one is warm.
49
+ *
50
+ * Serves a stale copy immediately and refreshes behind the caller, so an edit
51
+ * takes effect within a TTL without any request ever paying for the fetch.
52
+ *
53
+ * Takes an id or a slug — the server resolves either. Cached under whatever
54
+ * you passed, so pulling the same prompt both ways keeps two entries.
55
+ */
56
+ async pull(handle) {
57
+ const payload = await pullWithCache(this.cache, handle, async () => {
58
+ const res = await this.sdk.prompts.get(handle);
59
+ return res.prompt;
60
+ });
61
+ return makePrompt({
62
+ id: payload.id,
63
+ name: payload.name,
64
+ slug: payload.slug ?? null,
65
+ blocks: payload.blocks,
66
+ question: payload.question,
67
+ fields: payload.fieldSpec ?? [],
68
+ versionId: payload.currentVersionId ?? null,
69
+ models: payload.models ?? [],
70
+ }, "server", this.recorder());
71
+ }
72
+ /** A prompt written in code, wired to record its usage through this client. */
73
+ build(spec, opts = {}) {
74
+ return buildCustomPrompt(spec, { ...opts, sdk: this.sdk });
75
+ }
76
+ /**
77
+ * Said once, because this package cannot count tokens it never saw.
78
+ *
79
+ * A callback that forgets to return them records a rollout reading zero in
80
+ * and zero out, which prices at nothing and quietly drags every average that
81
+ * includes it. Silence here is how that goes unnoticed for a month.
82
+ */
83
+ warnOnEmptyTokens(outcome) {
84
+ if (this.warnedAboutTokens)
85
+ return;
86
+ if (outcome.status === "failed")
87
+ return;
88
+ if ((outcome.inputTokens ?? 0) > 0 || (outcome.outputTokens ?? 0) > 0)
89
+ return;
90
+ this.warnedAboutTokens = true;
91
+ console.warn("[prompt] trace() recorded a rollout with no tokens. This package never sees " +
92
+ "the provider response, so return inputTokens and outputTokens from your callback " +
93
+ "or every cost on this prompt reads as zero.");
94
+ }
95
+ /**
96
+ * Runs the prompt server-side and records the rollout.
97
+ *
98
+ * The batch path. A production request should pull, render and report
99
+ * instead — this adds a hop and makes spendgraph a dependency of the caller's
100
+ * uptime, which is the right trade for evaluation and the wrong one for a
101
+ * user waiting on a response.
102
+ */
103
+ async run(promptId, values = {}, opts = {}) {
104
+ return runOnce(this.sdk.prompts, promptId, values, opts, this.budget);
105
+ }
106
+ /** k repetitions of one case. Always k results, failures included. */
107
+ async sample(promptId, values = {}, opts = {}) {
108
+ return sampleRuns(this.sdk.prompts, promptId, values, { concurrency: this.concurrency, ...opts }, this.budget);
109
+ }
110
+ /** One rollout per model — the playground's comparison, headless. */
111
+ async runAll(promptId, values = {}, opts = {}) {
112
+ const models = opts.models ?? (await this.pull(promptId)).models;
113
+ if (models.length === 0) {
114
+ throw new Error("No models given and the prompt has none saved.");
115
+ }
116
+ return runAcrossModels(this.sdk.prompts, promptId, values, models, { concurrency: this.concurrency, ...opts }, this.budget);
117
+ }
118
+ /** The dataset a prompt is evaluated against, with its split counts. */
119
+ async cases(promptId) {
120
+ return this.sdk.prompts.cases(promptId);
121
+ }
122
+ /**
123
+ * Replaces the whole dataset.
124
+ *
125
+ * A dataset is a set: "which cases am I evaluating against" has one answer at
126
+ * a time, and a merge would leave no way to remove a case or to know from the
127
+ * outside what the set currently is.
128
+ */
129
+ async setCases(promptId, cases) {
130
+ return this.sdk.prompts.putCases(promptId, cases);
131
+ }
132
+ /** Micro-dollars this client has spent on runs it initiated. */
133
+ spent() {
134
+ return this.budget.spent();
135
+ }
136
+ /** What is left of the ceiling, or Infinity when none was set. */
137
+ remaining() {
138
+ return this.budget.remaining();
139
+ }
140
+ /** Every wording this prompt has had, newest first. */
141
+ async versions(promptId, opts = {}) {
142
+ return this.sdk.prompts.versions(promptId, opts);
143
+ }
144
+ /**
145
+ * Serves this version from now on.
146
+ *
147
+ * Invalidates the cache: a promotion that left a warm entry in place would
148
+ * keep sending the old wording for up to a TTL, which is the one moment a
149
+ * caller is actively watching for the change.
150
+ */
151
+ async promote(promptId, versionId) {
152
+ const res = await this.sdk.prompts.promote(promptId, versionId);
153
+ this.invalidate(promptId);
154
+ return res;
155
+ }
156
+ /**
157
+ * Drops whatever is cached, so the next pull refetches.
158
+ *
159
+ * Both handles, not just the one passed. `pull` keys the cache on whatever
160
+ * string it was given, so the same prompt can sit under its uuid and its
161
+ * slug at once — and `promote` is only reachable with a uuid while the
162
+ * dashboard teaches pulling by slug.
163
+ */
164
+ invalidate(promptId) {
165
+ if (!promptId) {
166
+ this.cache.clear();
167
+ return;
168
+ }
169
+ for (const key of this.cache.keysWhere((p) => p.id === promptId || p.slug === promptId)) {
170
+ this.cache.delete(key);
171
+ }
172
+ this.cache.delete(promptId);
173
+ this.cache.invalidate(promptId);
174
+ }
175
+ /**
176
+ * Records a rollout the caller executed.
177
+ *
178
+ * Never throws and never rejects. This sits beside a user-facing request that
179
+ * has already been answered; telemetry that can break the thing it measures
180
+ * is worse than no telemetry.
181
+ */
182
+ /**
183
+ * Records a rollout and returns what the server priced it at, in micro-USD,
184
+ * or undefined where the model had no price to compute one from.
185
+ *
186
+ * The price is computed there, from the tokens and the project's pricing
187
+ * table, so this is the only place it exists — and it is why `report` cannot
188
+ * simply be reused: that one answers with the rollout id.
189
+ *
190
+ * `priced: false` is answered as undefined rather than as the stored zero.
191
+ * The column is `notNull`, so an unpriced rollout is written as zero and the
192
+ * flag beside it is the only thing that tells free apart from unknown.
193
+ */
194
+ async priced(promptId, input) {
195
+ const rolloutId = input.rolloutId ?? newRolloutId();
196
+ if (!this.hasKey)
197
+ return undefined;
198
+ try {
199
+ const res = (await this.sdk.prompts.report(promptId, { ...input, rolloutId }));
200
+ if (res.priced === false)
201
+ return undefined;
202
+ return res.rollout?.costMicros;
203
+ }
204
+ catch (err) {
205
+ this.onReportError?.(err);
206
+ return undefined;
207
+ }
208
+ }
209
+ async report(promptId, input) {
210
+ const rolloutId = input.rolloutId ?? newRolloutId();
211
+ if (!this.hasKey)
212
+ return rolloutId;
213
+ try {
214
+ await this.sdk.prompts.report(promptId, { ...input, rolloutId });
215
+ }
216
+ catch (err) {
217
+ this.onReportError?.(err);
218
+ }
219
+ return rolloutId;
220
+ }
221
+ }
222
+ /**
223
+ * One prompt from the server, without keeping a client around.
224
+ *
225
+ * Convenience over `new PromptClient(...).pull(...)`, and it builds a client
226
+ * per call — a service pulling repeatedly wants the class, which caches.
227
+ */
228
+ export function pullPrompt(handle, opts) {
229
+ return new PromptClient(opts).pull(handle);
230
+ }
@@ -0,0 +1,11 @@
1
+ export type { FieldError, FieldSpec, FieldType, RolloutStep } from "@spendgraph/sdk";
2
+ export { FieldValidationError } from "@spendgraph/sdk";
3
+ export { BudgetExceededError } from "./budget/index.js";
4
+ export type { BuildOptions } from "./build/index.js";
5
+ export { buildCustomPrompt } from "./build/index.js";
6
+ export type { PromptClientOptions } from "./client.js";
7
+ export { PromptClient, pullPrompt } from "./client.js";
8
+ export type { Answer, BatchOptions, Chain, InvokeOptions, Model } from "./prompt/index.js";
9
+ export { MAX_RECORDED_MESSAGES } from "./prompt/index.js";
10
+ export type { Block, History, Message } from "./render/index.js";
11
+ export type { DatasetCase, DatasetSummary, Prompt, PromptSource, PromptSpec, PromptVersion, RenderOptions, ReportInput, RunOptions, RunResult, Split, TraceHandle, TraceOptions, TraceOutcome, } from "./types/index.js";
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ export { FieldValidationError } from "@spendgraph/sdk";
2
+ export { BudgetExceededError } from "./budget/index.js";
3
+ export { buildCustomPrompt } from "./build/index.js";
4
+ export { PromptClient, pullPrompt } from "./client.js";
5
+ export { MAX_RECORDED_MESSAGES } from "./prompt/index.js";
@@ -0,0 +1,17 @@
1
+ /**
2
+ * The pieces `PromptClient` is built from.
3
+ *
4
+ * Not in the main entry on purpose: everything here is already wired for you,
5
+ * and a caller reaching for it is building their own client rather than using
6
+ * this one. Kept exported so that stays possible.
7
+ */
8
+ export type { RolloutInput, RolloutRecord } from "@spendgraph/sdk";
9
+ export { serializeFields, validateFields } from "@spendgraph/sdk";
10
+ export { Budget } from "./budget/index.js";
11
+ export type { CacheHit, CacheOptions } from "./cache/index.js";
12
+ export { PullCache } from "./cache/index.js";
13
+ export type { ModelOptions, PromptShape, Recorder } from "./prompt/index.js";
14
+ export { bind, makePrompt, noRecorder } from "./prompt/index.js";
15
+ export { pullWithCache } from "./pull/index.js";
16
+ export { mapLimit, newRolloutId } from "./run/index.js";
17
+ export type { PromptPayload, Recordable, Rollout, TurnRecord, TurnSource } from "./types/index.js";
@@ -0,0 +1,13 @@
1
+ /**
2
+ * The pieces `PromptClient` is built from.
3
+ *
4
+ * Not in the main entry on purpose: everything here is already wired for you,
5
+ * and a caller reaching for it is building their own client rather than using
6
+ * this one. Kept exported so that stays possible.
7
+ */
8
+ export { serializeFields, validateFields } from "@spendgraph/sdk";
9
+ export { Budget } from "./budget/index.js";
10
+ export { PullCache } from "./cache/index.js";
11
+ export { bind, makePrompt, noRecorder } from "./prompt/index.js";
12
+ export { pullWithCache } from "./pull/index.js";
13
+ export { mapLimit, newRolloutId } from "./run/index.js";