@herbertgao/pi-extensions 2026.9.7 → 2026.9.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -4
- package/THIRD_PARTY_NOTICES.md +52 -0
- package/node_modules/@herbertgao/pi-bark/package.json +2 -2
- package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/README.md +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/package.json +4 -4
- package/node_modules/@herbertgao/resume-from/package.json +2 -2
- package/node_modules/@narumitw/pi-btw/README.md +6 -1
- package/node_modules/@narumitw/pi-btw/dist/index.ts +209 -23
- package/node_modules/@narumitw/pi-btw/dist/index.ts.map +4 -4
- package/node_modules/@narumitw/pi-btw/package.json +1 -1
- package/node_modules/@narumitw/pi-btw/src/btw.ts +1 -12
- package/node_modules/@narumitw/pi-btw/src/menu.ts +244 -15
- package/node_modules/@narumitw/pi-btw/src/settings.ts +5 -0
- package/node_modules/grok-mermaid/CHANGELOG.md +46 -0
- package/node_modules/grok-mermaid/LICENSE +205 -0
- package/node_modules/grok-mermaid/README.md +191 -0
- package/node_modules/grok-mermaid/dist/ansi.d.ts +16 -0
- package/node_modules/grok-mermaid/dist/ansi.d.ts.map +1 -0
- package/node_modules/grok-mermaid/dist/ansi.js +23 -0
- package/node_modules/grok-mermaid/dist/ansi.js.map +1 -0
- package/node_modules/grok-mermaid/dist/canvas.d.ts +87 -0
- package/node_modules/grok-mermaid/dist/canvas.d.ts.map +1 -0
- package/node_modules/grok-mermaid/dist/canvas.js +366 -0
- package/node_modules/grok-mermaid/dist/canvas.js.map +1 -0
- package/node_modules/grok-mermaid/dist/graph.d.ts +74 -0
- package/node_modules/grok-mermaid/dist/graph.d.ts.map +1 -0
- package/node_modules/grok-mermaid/dist/graph.js +91 -0
- package/node_modules/grok-mermaid/dist/graph.js.map +1 -0
- package/node_modules/grok-mermaid/dist/index.d.ts +32 -0
- package/node_modules/grok-mermaid/dist/index.d.ts.map +1 -0
- package/node_modules/grok-mermaid/dist/index.js +100 -0
- package/node_modules/grok-mermaid/dist/index.js.map +1 -0
- package/node_modules/grok-mermaid/dist/labels.d.ts +62 -0
- package/node_modules/grok-mermaid/dist/labels.d.ts.map +1 -0
- package/node_modules/grok-mermaid/dist/labels.js +324 -0
- package/node_modules/grok-mermaid/dist/labels.js.map +1 -0
- package/node_modules/grok-mermaid/dist/layout-seq.d.ts +12 -0
- package/node_modules/grok-mermaid/dist/layout-seq.d.ts.map +1 -0
- package/node_modules/grok-mermaid/dist/layout-seq.js +194 -0
- package/node_modules/grok-mermaid/dist/layout-seq.js.map +1 -0
- package/node_modules/grok-mermaid/dist/layout.d.ts +87 -0
- package/node_modules/grok-mermaid/dist/layout.d.ts.map +1 -0
- package/node_modules/grok-mermaid/dist/layout.js +881 -0
- package/node_modules/grok-mermaid/dist/layout.js.map +1 -0
- package/node_modules/grok-mermaid/dist/parse.d.ts +83 -0
- package/node_modules/grok-mermaid/dist/parse.d.ts.map +1 -0
- package/node_modules/grok-mermaid/dist/parse.js +1151 -0
- package/node_modules/grok-mermaid/dist/parse.js.map +1 -0
- package/node_modules/grok-mermaid/dist/source-box.d.ts +18 -0
- package/node_modules/grok-mermaid/dist/source-box.d.ts.map +1 -0
- package/node_modules/grok-mermaid/dist/source-box.js +78 -0
- package/node_modules/grok-mermaid/dist/source-box.js.map +1 -0
- package/node_modules/grok-mermaid/dist/types.d.ts +42 -0
- package/node_modules/grok-mermaid/dist/types.d.ts.map +1 -0
- package/node_modules/grok-mermaid/dist/types.js +1 -0
- package/node_modules/grok-mermaid/dist/types.js.map +1 -0
- package/node_modules/grok-mermaid/dist/width-data.d.ts +2 -0
- package/node_modules/grok-mermaid/dist/width-data.d.ts.map +1 -0
- package/node_modules/grok-mermaid/dist/width-data.js +994 -0
- package/node_modules/grok-mermaid/dist/width-data.js.map +1 -0
- package/node_modules/grok-mermaid/dist/width.d.ts +18 -0
- package/node_modules/grok-mermaid/dist/width.d.ts.map +1 -0
- package/node_modules/grok-mermaid/dist/width.js +76 -0
- package/node_modules/grok-mermaid/dist/width.js.map +1 -0
- package/node_modules/grok-mermaid/package.json +49 -0
- package/node_modules/grok-mermaid/src/ansi.ts +34 -0
- package/node_modules/grok-mermaid/src/canvas.ts +373 -0
- package/node_modules/grok-mermaid/src/graph.ts +142 -0
- package/node_modules/grok-mermaid/src/index.ts +104 -0
- package/node_modules/grok-mermaid/src/labels.ts +326 -0
- package/node_modules/grok-mermaid/src/layout-seq.ts +203 -0
- package/node_modules/grok-mermaid/src/layout.ts +1015 -0
- package/node_modules/grok-mermaid/src/parse.ts +1189 -0
- package/node_modules/grok-mermaid/src/source-box.ts +89 -0
- package/node_modules/grok-mermaid/src/types.ts +43 -0
- package/node_modules/grok-mermaid/src/width-data.ts +993 -0
- package/node_modules/grok-mermaid/src/width.ts +74 -0
- package/node_modules/pi-jev-auto-mode/CHANGELOG.md +110 -0
- package/node_modules/pi-jev-auto-mode/LICENSE +21 -0
- package/node_modules/pi-jev-auto-mode/README.md +285 -0
- package/node_modules/pi-jev-auto-mode/SECURITY.md +32 -0
- package/node_modules/pi-jev-auto-mode/docs/calibration.md +154 -0
- package/node_modules/pi-jev-auto-mode/docs/design.md +164 -0
- package/node_modules/pi-jev-auto-mode/docs/security.md +124 -0
- package/node_modules/pi-jev-auto-mode/index.ts +1 -0
- package/node_modules/pi-jev-auto-mode/package.json +72 -0
- package/node_modules/pi-jev-auto-mode/src/call.ts +180 -0
- package/node_modules/pi-jev-auto-mode/src/decide.ts +86 -0
- package/node_modules/pi-jev-auto-mode/src/extension.ts +862 -0
- package/node_modules/pi-jev-auto-mode/src/intent.ts +71 -0
- package/node_modules/pi-jev-auto-mode/src/jev/availability.ts +53 -0
- package/node_modules/pi-jev-auto-mode/src/jev/criteria.ts +19 -0
- package/node_modules/pi-jev-auto-mode/src/jev/decide.ts +187 -0
- package/node_modules/pi-jev-auto-mode/src/jev/engine.ts +165 -0
- package/node_modules/pi-jev-auto-mode/src/jev/index.ts +20 -0
- package/node_modules/pi-jev-auto-mode/src/jev/questions.ts +273 -0
- package/node_modules/pi-jev-auto-mode/src/jev/response.ts +64 -0
- package/node_modules/pi-jev-auto-mode/src/jev/state.ts +20 -0
- package/node_modules/pi-jev-auto-mode/src/jev/transport.ts +117 -0
- package/node_modules/pi-jev-auto-mode/src/jev/types.ts +46 -0
- package/node_modules/pi-jev-auto-mode/src/policy.ts +558 -0
- package/node_modules/pi-jev-auto-mode/src/records.ts +118 -0
- package/node_modules/pi-jev-auto-mode/src/settings.ts +327 -0
- package/node_modules/pi-jev-auto-mode/src/ui.ts +233 -0
- package/node_modules/pi-lens/CHANGELOG.md +45 -0
- package/node_modules/pi-lens/dist/clients/actionable-warnings.js +21 -9
- package/node_modules/pi-lens/dist/clients/cascade-format.js +24 -0
- package/node_modules/pi-lens/dist/clients/extension-log.js +48 -1
- package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +11 -12
- package/node_modules/pi-lens/dist/clients/lsp/cascade-tier.js +3 -0
- package/node_modules/pi-lens/dist/clients/lsp/index.js +41 -7
- package/node_modules/pi-lens/dist/clients/mcp/analyze.js +9 -2
- package/node_modules/pi-lens/dist/clients/path-utils.js +51 -9
- package/node_modules/pi-lens/dist/clients/persistent-reverify.js +271 -0
- package/node_modules/pi-lens/dist/clients/pipeline.js +15 -1
- package/node_modules/pi-lens/dist/clients/runtime-turn.js +124 -7
- package/node_modules/pi-lens/dist/clients/skills-resolver.js +38 -2
- package/node_modules/pi-lens/dist/clients/widget-state.js +152 -6
- package/node_modules/pi-lens/dist/index.js +729 -344
- package/node_modules/pi-lens/dist/tools/lens-diagnostic-mark.js +14 -2
- package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +84 -17
- package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +32 -5
- package/node_modules/pi-lens/package.json +1 -1
- package/node_modules/pi-typesafe/LICENSE +21 -0
- package/node_modules/pi-typesafe/README.md +153 -0
- package/node_modules/pi-typesafe/dist/ask.d.ts +32 -0
- package/node_modules/pi-typesafe/dist/ask.js +25 -0
- package/node_modules/pi-typesafe/dist/auth.d.ts +56 -0
- package/node_modules/pi-typesafe/dist/auth.js +114 -0
- package/node_modules/pi-typesafe/dist/batch.d.ts +74 -0
- package/node_modules/pi-typesafe/dist/batch.js +116 -0
- package/node_modules/pi-typesafe/dist/calibrate.d.ts +106 -0
- package/node_modules/pi-typesafe/dist/calibrate.js +157 -0
- package/node_modules/pi-typesafe/dist/client.d.ts +69 -0
- package/node_modules/pi-typesafe/dist/client.js +188 -0
- package/node_modules/pi-typesafe/dist/credentials.d.ts +46 -0
- package/node_modules/pi-typesafe/dist/credentials.js +112 -0
- package/node_modules/pi-typesafe/dist/errors.d.ts +9 -0
- package/node_modules/pi-typesafe/dist/errors.js +31 -0
- package/node_modules/pi-typesafe/dist/extension.d.ts +3 -0
- package/node_modules/pi-typesafe/dist/extension.js +222 -0
- package/node_modules/pi-typesafe/dist/index.d.ts +18 -0
- package/node_modules/pi-typesafe/dist/index.js +9 -0
- package/node_modules/pi-typesafe/dist/key-prompt.d.ts +3 -0
- package/node_modules/pi-typesafe/dist/key-prompt.js +43 -0
- package/node_modules/pi-typesafe/dist/login.d.ts +27 -0
- package/node_modules/pi-typesafe/dist/login.js +38 -0
- package/node_modules/pi-typesafe/dist/schema.d.ts +43 -0
- package/node_modules/pi-typesafe/dist/schema.js +152 -0
- package/node_modules/pi-typesafe/dist/ui.d.ts +3 -0
- package/node_modules/pi-typesafe/dist/ui.js +3 -0
- package/node_modules/pi-typesafe/dist/usage.d.ts +73 -0
- package/node_modules/pi-typesafe/dist/usage.js +182 -0
- package/node_modules/pi-typesafe/examples/decision-extension.ts +41 -0
- package/node_modules/pi-typesafe/extensions/index.js +2 -0
- package/node_modules/pi-typesafe/package.json +89 -0
- package/package.json +16 -8
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
/** Hidden input in the TUI; falls back to Pi's plain input dialog where custom components are unavailable. */
|
|
3
|
+
export declare function promptForApiKey(ctx: ExtensionCommandContext): Promise<string | undefined>;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Container, CURSOR_MARKER, Input, Key, matchesKey, Text, truncateToWidth } from "@earendil-works/pi-tui";
|
|
2
|
+
/** Single-line input that renders bullets instead of the typed value. */
|
|
3
|
+
class SecretInput extends Input {
|
|
4
|
+
render(width) {
|
|
5
|
+
const length = [...this.getValue()].length;
|
|
6
|
+
const bullets = "•".repeat(Math.min(length, Math.max(0, width - 2)));
|
|
7
|
+
const cursor = this.focused ? `${CURSOR_MARKER}\x1b[7m \x1b[27m` : "";
|
|
8
|
+
return [truncateToWidth(bullets + cursor, width, "")];
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
class KeyPrompt extends Container {
|
|
12
|
+
input = new SecretInput();
|
|
13
|
+
isFocused = false;
|
|
14
|
+
constructor(theme, done) {
|
|
15
|
+
super();
|
|
16
|
+
this.addChild(new Text(theme.fg("accent", theme.bold("TypeSafe API key")), 1, 0));
|
|
17
|
+
this.addChild(new Text(theme.fg("muted", "Paste the key from console.typesafe.ai › API Keys. Input is hidden. Enter saves, Esc cancels."), 1, 0));
|
|
18
|
+
this.addChild(this.input);
|
|
19
|
+
this.input.onSubmit = value => done(value);
|
|
20
|
+
this.input.onEscape = () => done(undefined);
|
|
21
|
+
}
|
|
22
|
+
get focused() {
|
|
23
|
+
return this.isFocused;
|
|
24
|
+
}
|
|
25
|
+
set focused(value) {
|
|
26
|
+
this.isFocused = value;
|
|
27
|
+
this.input.focused = value;
|
|
28
|
+
}
|
|
29
|
+
handleInput(data) {
|
|
30
|
+
if (matchesKey(data, Key.escape)) {
|
|
31
|
+
this.input.onEscape?.();
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
this.input.handleInput(data);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/** Hidden input in the TUI; falls back to Pi's plain input dialog where custom components are unavailable. */
|
|
38
|
+
export async function promptForApiKey(ctx) {
|
|
39
|
+
if (typeof ctx.ui.custom === "function") {
|
|
40
|
+
return ctx.ui.custom((_tui, theme, _keybindings, done) => new KeyPrompt(theme, done));
|
|
41
|
+
}
|
|
42
|
+
return ctx.ui.input("TypeSafe API key (visible while typing)", "Paste the key, then press Enter");
|
|
43
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { KeySource } from "./credentials.js";
|
|
3
|
+
export interface LoginResult {
|
|
4
|
+
/** Where the verified key was saved. */
|
|
5
|
+
path: string;
|
|
6
|
+
/** Number of models the key can access; proves the key was accepted by the API. */
|
|
7
|
+
models: number;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Prompt for a key (hidden input), verify it with a model listing, and store it for every pi-typesafe consumer.
|
|
11
|
+
* Resolves to undefined when the user cancels. Rejects with TypeSafeIntegrationError for an invalid or unverifiable key.
|
|
12
|
+
* Refuses when TYPESAFE_API_KEY is set, because the environment would shadow the stored key.
|
|
13
|
+
*/
|
|
14
|
+
export declare function loginWithPrompt(ctx: ExtensionCommandContext): Promise<LoginResult | undefined>;
|
|
15
|
+
export type EnsureApiKeyResult = {
|
|
16
|
+
source: KeySource;
|
|
17
|
+
login?: undefined;
|
|
18
|
+
} | {
|
|
19
|
+
source: "stored";
|
|
20
|
+
login: LoginResult;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Use the configured key if there is one; otherwise run the login prompt. `undefined` means the user cancelled.
|
|
24
|
+
* A store that must not be read throws `configuration` with the reason instead of prompting, so a permissions
|
|
25
|
+
* problem stays visible; the result shape is frozen for existing callers.
|
|
26
|
+
*/
|
|
27
|
+
export declare function ensureApiKey(ctx: ExtensionCommandContext): Promise<EnsureApiKeyResult | undefined>;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { createTypeSafe } from "./client.js";
|
|
2
|
+
import { keySituation, normalizeApiKey, storeApiKey } from "./credentials.js";
|
|
3
|
+
import { TypeSafeIntegrationError } from "./errors.js";
|
|
4
|
+
import { promptForApiKey } from "./key-prompt.js";
|
|
5
|
+
/**
|
|
6
|
+
* Prompt for a key (hidden input), verify it with a model listing, and store it for every pi-typesafe consumer.
|
|
7
|
+
* Resolves to undefined when the user cancels. Rejects with TypeSafeIntegrationError for an invalid or unverifiable key.
|
|
8
|
+
* Refuses when TYPESAFE_API_KEY is set, because the environment would shadow the stored key.
|
|
9
|
+
*/
|
|
10
|
+
export async function loginWithPrompt(ctx) {
|
|
11
|
+
if (process.env.TYPESAFE_API_KEY?.trim()) {
|
|
12
|
+
throw new TypeSafeIntegrationError("configuration", "TYPESAFE_API_KEY is set in the environment and takes precedence over a stored key. Unset it before logging in interactively.");
|
|
13
|
+
}
|
|
14
|
+
if (!ctx.hasUI) {
|
|
15
|
+
throw new TypeSafeIntegrationError("configuration", "Logging in needs an interactive session. Set TYPESAFE_API_KEY in the environment instead.");
|
|
16
|
+
}
|
|
17
|
+
const entered = await promptForApiKey(ctx);
|
|
18
|
+
if (entered === undefined)
|
|
19
|
+
return undefined;
|
|
20
|
+
const key = normalizeApiKey(entered);
|
|
21
|
+
// Verify before saving so a bad paste fails here, not on first use.
|
|
22
|
+
const models = await createTypeSafe({ apiKey: key }).listModels();
|
|
23
|
+
return { path: storeApiKey(key), models: models.length };
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Use the configured key if there is one; otherwise run the login prompt. `undefined` means the user cancelled.
|
|
27
|
+
* A store that must not be read throws `configuration` with the reason instead of prompting, so a permissions
|
|
28
|
+
* problem stays visible; the result shape is frozen for existing callers.
|
|
29
|
+
*/
|
|
30
|
+
export async function ensureApiKey(ctx) {
|
|
31
|
+
const situation = keySituation();
|
|
32
|
+
if (situation.kind === "environment" || situation.kind === "stored")
|
|
33
|
+
return { source: situation.kind };
|
|
34
|
+
if (situation.kind === "unusable")
|
|
35
|
+
throw new TypeSafeIntegrationError("configuration", situation.reason);
|
|
36
|
+
const login = await loginWithPrompt(ctx);
|
|
37
|
+
return login ? { source: "stored", login } : undefined;
|
|
38
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { SystemOneRequest } from "@typesafe-ai/sdk";
|
|
2
|
+
import { Type } from "typebox";
|
|
3
|
+
/** Default UTF-8 JSON byte budget for one evaluation request; the tool and the client share it. */
|
|
4
|
+
export declare const DEFAULT_MAX_INPUT_BYTES = 65536;
|
|
5
|
+
/** Questions one request may ask. More than this needs `chunkEvaluationRequest`, which splits and fans out. */
|
|
6
|
+
export declare const DEFAULT_MAX_QUESTIONS = 32;
|
|
7
|
+
/** The JSON schema used by both the Pi tool and the programmatic interface. */
|
|
8
|
+
export declare const evaluationSchema: Type.TObject<{
|
|
9
|
+
state: Type.TUnion<[Type.TString, Type.TNull, Type.TArray<Type.TUnknown>, Type.TRecord<"^.*$", Type.TUnknown>]>;
|
|
10
|
+
questions: Type.TRecord<"^.*$", Type.TUnion<[Type.TObject<{
|
|
11
|
+
type: Type.TLiteral<"noul">;
|
|
12
|
+
instructions: Type.TOptional<Type.TUnion<[Type.TString, Type.TNull, Type.TArray<Type.TUnknown>, Type.TRecord<"^.*$", Type.TUnknown>]>>;
|
|
13
|
+
criteria: Type.TOptional<Type.TUnion<[Type.TNull, Type.TObject<{
|
|
14
|
+
true: Type.TOptional<Type.TUnion<[Type.TString, Type.TNull, Type.TArray<Type.TUnknown>, Type.TRecord<"^.*$", Type.TUnknown>]>>;
|
|
15
|
+
false: Type.TOptional<Type.TUnion<[Type.TString, Type.TNull, Type.TArray<Type.TUnknown>, Type.TRecord<"^.*$", Type.TUnknown>]>>;
|
|
16
|
+
}>]>>;
|
|
17
|
+
}>, Type.TObject<{
|
|
18
|
+
type: Type.TLiteral<"choice">;
|
|
19
|
+
instructions: Type.TOptional<Type.TUnion<[Type.TString, Type.TNull, Type.TArray<Type.TUnknown>, Type.TRecord<"^.*$", Type.TUnknown>]>>;
|
|
20
|
+
criteria: Type.TRecord<"^.*$", Type.TUnion<[Type.TString, Type.TNull, Type.TArray<Type.TUnknown>, Type.TRecord<"^.*$", Type.TUnknown>]>>;
|
|
21
|
+
}>, Type.TObject<{
|
|
22
|
+
type: Type.TLiteral<"score">;
|
|
23
|
+
instructions: Type.TOptional<Type.TUnion<[Type.TString, Type.TNull, Type.TArray<Type.TUnknown>, Type.TRecord<"^.*$", Type.TUnknown>]>>;
|
|
24
|
+
criteria: Type.TArray<Type.TUnion<[Type.TString, Type.TNull, Type.TArray<Type.TUnknown>, Type.TRecord<"^.*$", Type.TUnknown>]>>;
|
|
25
|
+
}>]>>;
|
|
26
|
+
model: Type.TOptional<Type.TString>;
|
|
27
|
+
}>;
|
|
28
|
+
/** Validate without including submitted content in validation errors. Prefer prepareEvaluationRequest(), which also accepts near-misses and enforces the byte budget. */
|
|
29
|
+
export declare function parseEvaluationRequest(value: unknown): SystemOneRequest;
|
|
30
|
+
/** Accept common near-misses from language models without loosening the schema itself. Prefer prepareEvaluationRequest(), which applies this before validating. */
|
|
31
|
+
export declare function normalizeEvaluationRequest(value: unknown): unknown;
|
|
32
|
+
export interface PrepareEvaluationOptions {
|
|
33
|
+
/** UTF-8 JSON bytes of the serialized request. Default: DEFAULT_MAX_INPUT_BYTES. */
|
|
34
|
+
maxInputBytes?: number;
|
|
35
|
+
}
|
|
36
|
+
/** One byte rule for every limit check: measure the serialized request and name the configured limit. */
|
|
37
|
+
export declare function assertWithinByteLimit(text: string, maxInputBytes: number): void;
|
|
38
|
+
/**
|
|
39
|
+
* The one admission rule: normalize known near-miss aliases, validate the schema and JSON-safety, then enforce the byte
|
|
40
|
+
* budget — always in that order. The Pi tool, the playground, and client.evaluate() all pass through here, so what one
|
|
41
|
+
* accepts the others accept.
|
|
42
|
+
*/
|
|
43
|
+
export declare function prepareEvaluationRequest(value: unknown, options?: PrepareEvaluationOptions): SystemOneRequest;
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { Type } from "typebox";
|
|
2
|
+
import { Check, Errors } from "typebox/value";
|
|
3
|
+
import { TypeSafeIntegrationError } from "./errors.js";
|
|
4
|
+
/** Default UTF-8 JSON byte budget for one evaluation request; the tool and the client share it. */
|
|
5
|
+
export const DEFAULT_MAX_INPUT_BYTES = 65_536;
|
|
6
|
+
/** Questions one request may ask. More than this needs `chunkEvaluationRequest`, which splits and fans out. */
|
|
7
|
+
export const DEFAULT_MAX_QUESTIONS = 32;
|
|
8
|
+
// The API accepts structured descriptions, not only strings.
|
|
9
|
+
const entry = Type.Union([
|
|
10
|
+
Type.String(),
|
|
11
|
+
Type.Null(),
|
|
12
|
+
Type.Array(Type.Unknown()),
|
|
13
|
+
Type.Record(Type.String(), Type.Unknown()),
|
|
14
|
+
]);
|
|
15
|
+
const instructions = Type.Optional(entry);
|
|
16
|
+
const question = Type.Union([
|
|
17
|
+
Type.Object({
|
|
18
|
+
type: Type.Literal("noul"),
|
|
19
|
+
instructions,
|
|
20
|
+
criteria: Type.Optional(Type.Union([
|
|
21
|
+
Type.Null(),
|
|
22
|
+
Type.Object({ true: Type.Optional(entry), false: Type.Optional(entry) }, { additionalProperties: false }),
|
|
23
|
+
])),
|
|
24
|
+
}, { additionalProperties: false }),
|
|
25
|
+
Type.Object({
|
|
26
|
+
type: Type.Literal("choice"),
|
|
27
|
+
instructions,
|
|
28
|
+
criteria: Type.Record(Type.String({ minLength: 1, maxLength: 200 }), entry, { minProperties: 1, maxProperties: 64 }),
|
|
29
|
+
}, { additionalProperties: false }),
|
|
30
|
+
Type.Object({
|
|
31
|
+
type: Type.Literal("score"),
|
|
32
|
+
instructions,
|
|
33
|
+
criteria: Type.Array(entry, { minItems: 2, maxItems: 32 }),
|
|
34
|
+
}, { additionalProperties: false }),
|
|
35
|
+
]);
|
|
36
|
+
/** The JSON schema used by both the Pi tool and the programmatic interface. */
|
|
37
|
+
export const evaluationSchema = Type.Object({
|
|
38
|
+
state: entry,
|
|
39
|
+
questions: Type.Record(Type.String({ minLength: 1, maxLength: 100 }), question, {
|
|
40
|
+
minProperties: 1,
|
|
41
|
+
maxProperties: DEFAULT_MAX_QUESTIONS,
|
|
42
|
+
}),
|
|
43
|
+
model: Type.Optional(Type.String({ minLength: 1, maxLength: 100 })),
|
|
44
|
+
}, { additionalProperties: false });
|
|
45
|
+
const usage = `Expected { state, questions: { <id>: { type: "choice", instructions, criteria: { label: description|null } } | { type: "score", instructions, criteria: [level0, level1, ...] } | { type: "noul", instructions } } }; 1–${DEFAULT_MAX_QUESTIONS} questions, Choice 1–64 options, Score 2–32 levels.`;
|
|
46
|
+
/** Paths and messages only; never the submitted values. */
|
|
47
|
+
function describeSchemaErrors(value) {
|
|
48
|
+
const details = [];
|
|
49
|
+
for (const error of Errors(evaluationSchema, value)) {
|
|
50
|
+
const path = error.instancePath.replace(/^\//, "").replace(/\//g, ".") || "request";
|
|
51
|
+
details.push(`${path.slice(0, 120)}: ${error.message}`);
|
|
52
|
+
if (details.length === 3)
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
return details.join("; ");
|
|
56
|
+
}
|
|
57
|
+
/** Validate without including submitted content in validation errors. Prefer prepareEvaluationRequest(), which also accepts near-misses and enforces the byte budget. */
|
|
58
|
+
export function parseEvaluationRequest(value) {
|
|
59
|
+
if (value !== null && typeof value === "object" && !Array.isArray(value) && !isJsonSafe(value)) {
|
|
60
|
+
throw new TypeSafeIntegrationError("validation", `Invalid evaluation request: state and questions must be plain JSON. ${usage}`);
|
|
61
|
+
}
|
|
62
|
+
if (!Check(evaluationSchema, value)) {
|
|
63
|
+
throw new TypeSafeIntegrationError("validation", `Invalid evaluation request at ${describeSchemaErrors(value)}. ${usage}`);
|
|
64
|
+
}
|
|
65
|
+
return value;
|
|
66
|
+
}
|
|
67
|
+
function isJsonSafe(value) {
|
|
68
|
+
try {
|
|
69
|
+
// Walk before schema validation to reject cycles and non-JSON values.
|
|
70
|
+
// Object descriptors avoid executing getters while checking user data.
|
|
71
|
+
const ancestors = new Set();
|
|
72
|
+
const walk = (item, depth) => {
|
|
73
|
+
if (depth > 64)
|
|
74
|
+
throw new Error();
|
|
75
|
+
if (item === null || typeof item === "string" || typeof item === "boolean")
|
|
76
|
+
return;
|
|
77
|
+
if (typeof item === "number" && Number.isFinite(item))
|
|
78
|
+
return;
|
|
79
|
+
if (typeof item !== "object" || ancestors.has(item))
|
|
80
|
+
throw new Error();
|
|
81
|
+
if (!Array.isArray(item) && Object.getPrototypeOf(item) !== Object.prototype && Object.getPrototypeOf(item) !== null)
|
|
82
|
+
throw new Error();
|
|
83
|
+
ancestors.add(item);
|
|
84
|
+
if (Object.getOwnPropertySymbols(item).length)
|
|
85
|
+
throw new Error();
|
|
86
|
+
for (const [key, descriptor] of Object.entries(Object.getOwnPropertyDescriptors(item))) {
|
|
87
|
+
if (Array.isArray(item) && key === "length")
|
|
88
|
+
continue;
|
|
89
|
+
if (!descriptor.enumerable || descriptor.get || descriptor.set)
|
|
90
|
+
throw new Error();
|
|
91
|
+
// SDK helpers include optional object fields whose value is undefined.
|
|
92
|
+
// JSON serialization omits those fields; array elements must remain JSON.
|
|
93
|
+
if (descriptor.value === undefined && !Array.isArray(item))
|
|
94
|
+
continue;
|
|
95
|
+
walk(descriptor.value, depth + 1);
|
|
96
|
+
}
|
|
97
|
+
ancestors.delete(item);
|
|
98
|
+
};
|
|
99
|
+
walk(value, 0);
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/** Accept common near-misses from language models without loosening the schema itself. Prefer prepareEvaluationRequest(), which applies this before validating. */
|
|
107
|
+
export function normalizeEvaluationRequest(value) {
|
|
108
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
109
|
+
return value;
|
|
110
|
+
const request = value;
|
|
111
|
+
const questions = request.questions;
|
|
112
|
+
if (!questions || typeof questions !== "object" || Array.isArray(questions))
|
|
113
|
+
return value;
|
|
114
|
+
const normalized = {};
|
|
115
|
+
for (const [id, question] of Object.entries(questions)) {
|
|
116
|
+
if (!question || typeof question !== "object" || Array.isArray(question)) {
|
|
117
|
+
normalized[id] = question;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
const { options, levels, choices, ...rest } = question;
|
|
121
|
+
const item = { ...rest };
|
|
122
|
+
if (item.criteria === undefined) {
|
|
123
|
+
const alias = options ?? levels ?? choices;
|
|
124
|
+
if (alias !== undefined)
|
|
125
|
+
item.criteria = alias;
|
|
126
|
+
}
|
|
127
|
+
if (item.type === "choice" && Array.isArray(item.criteria) && item.criteria.every(label => typeof label === "string" && label)) {
|
|
128
|
+
item.criteria = Object.fromEntries(item.criteria.map(label => [label, null]));
|
|
129
|
+
}
|
|
130
|
+
if (item.type === "noul" && typeof item.criteria === "string") {
|
|
131
|
+
item.criteria = { true: item.criteria };
|
|
132
|
+
}
|
|
133
|
+
normalized[id] = item;
|
|
134
|
+
}
|
|
135
|
+
return { ...request, questions: normalized };
|
|
136
|
+
}
|
|
137
|
+
/** One byte rule for every limit check: measure the serialized request and name the configured limit. */
|
|
138
|
+
export function assertWithinByteLimit(text, maxInputBytes) {
|
|
139
|
+
if (Buffer.byteLength(text, "utf8") > maxInputBytes) {
|
|
140
|
+
throw new TypeSafeIntegrationError("validation", `Evaluation exceeds the ${maxInputBytes}-byte input limit.`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* The one admission rule: normalize known near-miss aliases, validate the schema and JSON-safety, then enforce the byte
|
|
145
|
+
* budget — always in that order. The Pi tool, the playground, and client.evaluate() all pass through here, so what one
|
|
146
|
+
* accepts the others accept.
|
|
147
|
+
*/
|
|
148
|
+
export function prepareEvaluationRequest(value, options = {}) {
|
|
149
|
+
const validated = parseEvaluationRequest(normalizeEvaluationRequest(value));
|
|
150
|
+
assertWithinByteLimit(JSON.stringify(validated), options.maxInputBytes ?? DEFAULT_MAX_INPUT_BYTES);
|
|
151
|
+
return validated;
|
|
152
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TypeSafe bills input tokens only; output is free. The default mirrors the $42-per-billion-input-token rate the README
|
|
3
|
+
* quotes, so a spend cap means something before anyone configures a price. Override it when the rate changes.
|
|
4
|
+
*/
|
|
5
|
+
export declare const DEFAULT_USD_PER_MTOK = 0.042;
|
|
6
|
+
/** Counters for one window (a session, or one local day). */
|
|
7
|
+
export interface UsageTotals {
|
|
8
|
+
readonly requestsStarted: number;
|
|
9
|
+
readonly requestsSucceeded: number;
|
|
10
|
+
readonly requestsFailed: number;
|
|
11
|
+
readonly inputTokens: number;
|
|
12
|
+
readonly outputTokens: number;
|
|
13
|
+
}
|
|
14
|
+
/** One day of persisted totals plus the cost estimate for that day. */
|
|
15
|
+
export interface UsageReport extends UsageTotals {
|
|
16
|
+
readonly day: string;
|
|
17
|
+
readonly estimatedUsd: number;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Client-side spend caps. Every field is optional; an absent field is unlimited. Session caps come from the client
|
|
21
|
+
* options only; day caps also read the `PI_TYPESAFE_MAX_*` environment variables so a headless run can bound itself
|
|
22
|
+
* without editing code. A cap that is reached raises a `budget` error before the next request leaves the process.
|
|
23
|
+
*/
|
|
24
|
+
export interface SpendCaps {
|
|
25
|
+
readonly maxRequests?: number;
|
|
26
|
+
readonly maxRequestsPerDay?: number;
|
|
27
|
+
readonly maxInputTokensPerDay?: number;
|
|
28
|
+
readonly maxUsdPerDay?: number;
|
|
29
|
+
}
|
|
30
|
+
/** The cap that stops the next request, with what it allows and what has been used today. */
|
|
31
|
+
export interface BlockedCap {
|
|
32
|
+
readonly cap: "requestsPerDay" | "inputTokensPerDay" | "usdPerDay";
|
|
33
|
+
readonly limit: number;
|
|
34
|
+
readonly used: number;
|
|
35
|
+
readonly day: string;
|
|
36
|
+
}
|
|
37
|
+
export declare function localDay(now?: Date): string;
|
|
38
|
+
/** The stored ledger. Alongside the key store so one directory holds every pi-typesafe file. */
|
|
39
|
+
export declare function usagePath(): string;
|
|
40
|
+
/** Input-token cost, rounded to a micro-dollar so the number stays readable. */
|
|
41
|
+
export declare function estimateUsd(inputTokens: number, usdPerMTok: number): number;
|
|
42
|
+
export declare function emptyTotals(): UsageTotals;
|
|
43
|
+
/** The caps a headless run may set without code: `PI_TYPESAFE_MAX_USD_PER_DAY` and its siblings. */
|
|
44
|
+
export declare function capsFromEnvironment(env?: NodeJS.ProcessEnv): SpendCaps;
|
|
45
|
+
/** Environment first for day caps, then the explicit option; a cap is never raised by the environment. */
|
|
46
|
+
export declare function mergeCaps(explicit: SpendCaps, environment: SpendCaps): SpendCaps;
|
|
47
|
+
export interface UsageLedgerOptions {
|
|
48
|
+
/** Defaults to usagePath(); tests point it at a temporary file. */
|
|
49
|
+
path?: string;
|
|
50
|
+
/** Clock for the local day and for rollover; injectable for tests. */
|
|
51
|
+
now?: () => Date;
|
|
52
|
+
usdPerMTok?: number;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* One day of persisted request, token, and cost totals, plus the caps that stop the next request. The in-memory copy is
|
|
56
|
+
* authoritative for this process; the file is the cross-process, across-restart record. Reads are defensive, writes are
|
|
57
|
+
* atomic and best-effort, and a day rolls over on the local date, so a long eval cannot accumulate forever unnoticed.
|
|
58
|
+
*/
|
|
59
|
+
export interface UsageLedger {
|
|
60
|
+
readonly path: string;
|
|
61
|
+
readonly usdPerMTok: number;
|
|
62
|
+
/** Today's totals, after any rollover. */
|
|
63
|
+
today(): UsageReport;
|
|
64
|
+
/** Count the attempt before it is submitted; a request that never returns still counts. */
|
|
65
|
+
recordStart(): void;
|
|
66
|
+
recordSuccess(inputTokens: number, outputTokens: number): void;
|
|
67
|
+
recordFailure(): void;
|
|
68
|
+
/** The reached day cap that blocks the next request, or undefined. The caller owns the caps. */
|
|
69
|
+
blocked(caps: SpendCaps): BlockedCap | undefined;
|
|
70
|
+
/** One line for status output: today's requests, tokens, and cost, with the caps that apply. */
|
|
71
|
+
describe(caps?: SpendCaps): string;
|
|
72
|
+
}
|
|
73
|
+
export declare function openUsageLedger(options?: UsageLedgerOptions): UsageLedger;
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { chmodSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { piTypesafeDir } from "./credentials.js";
|
|
4
|
+
/**
|
|
5
|
+
* TypeSafe bills input tokens only; output is free. The default mirrors the $42-per-billion-input-token rate the README
|
|
6
|
+
* quotes, so a spend cap means something before anyone configures a price. Override it when the rate changes.
|
|
7
|
+
*/
|
|
8
|
+
export const DEFAULT_USD_PER_MTOK = 0.042;
|
|
9
|
+
/** Ledger days kept on disk; older entries are dropped on the next write. */
|
|
10
|
+
const KEEP_DAYS = 31;
|
|
11
|
+
const USAGE_VERSION = 1;
|
|
12
|
+
export function localDay(now = new Date()) {
|
|
13
|
+
const pad = (value) => String(value).padStart(2, "0");
|
|
14
|
+
return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`;
|
|
15
|
+
}
|
|
16
|
+
/** The stored ledger. Alongside the key store so one directory holds every pi-typesafe file. */
|
|
17
|
+
export function usagePath() {
|
|
18
|
+
return join(piTypesafeDir(), "usage.json");
|
|
19
|
+
}
|
|
20
|
+
/** Input-token cost, rounded to a micro-dollar so the number stays readable. */
|
|
21
|
+
export function estimateUsd(inputTokens, usdPerMTok) {
|
|
22
|
+
return Math.round((inputTokens * usdPerMTok) / 1e6 * 1e6) / 1e6;
|
|
23
|
+
}
|
|
24
|
+
export function emptyTotals() {
|
|
25
|
+
return { requestsStarted: 0, requestsSucceeded: 0, requestsFailed: 0, inputTokens: 0, outputTokens: 0 };
|
|
26
|
+
}
|
|
27
|
+
function count(value) {
|
|
28
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : 0;
|
|
29
|
+
}
|
|
30
|
+
function totalsOf(value) {
|
|
31
|
+
const raw = (value ?? {});
|
|
32
|
+
return {
|
|
33
|
+
requestsStarted: count(raw.requestsStarted),
|
|
34
|
+
requestsSucceeded: count(raw.requestsSucceeded),
|
|
35
|
+
requestsFailed: count(raw.requestsFailed),
|
|
36
|
+
inputTokens: count(raw.inputTokens),
|
|
37
|
+
outputTokens: count(raw.outputTokens),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function readDays(path) {
|
|
41
|
+
try {
|
|
42
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
43
|
+
const days = parsed && typeof parsed === "object" ? parsed.days : undefined;
|
|
44
|
+
if (!days || typeof days !== "object" || Array.isArray(days))
|
|
45
|
+
return {};
|
|
46
|
+
const result = {};
|
|
47
|
+
for (const [day, totals] of Object.entries(days)) {
|
|
48
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(day))
|
|
49
|
+
result[day] = totalsOf(totals);
|
|
50
|
+
}
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
// A missing, unreadable, or corrupt ledger restarts today's count; it never blocks a request.
|
|
55
|
+
return {};
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function keepRecent(days, today) {
|
|
59
|
+
const names = Object.keys(days).sort();
|
|
60
|
+
const result = {};
|
|
61
|
+
for (const name of names.slice(-KEEP_DAYS))
|
|
62
|
+
result[name] = days[name];
|
|
63
|
+
result[today] = days[today] ?? emptyTotals();
|
|
64
|
+
return result;
|
|
65
|
+
}
|
|
66
|
+
/** Owner-only, atomic, and best-effort: a ledger this process cannot write never fails a request. */
|
|
67
|
+
function writeDays(path, days) {
|
|
68
|
+
const file = { version: USAGE_VERSION, days };
|
|
69
|
+
const temporary = `${path}.${process.pid}.tmp`;
|
|
70
|
+
try {
|
|
71
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
72
|
+
writeFileSync(temporary, `${JSON.stringify(file, null, 2)}\n`, { mode: 0o600, flag: "w" });
|
|
73
|
+
chmodSync(temporary, 0o600);
|
|
74
|
+
renameSync(temporary, path);
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
try {
|
|
78
|
+
rmSync(temporary, { force: true });
|
|
79
|
+
}
|
|
80
|
+
catch { /* best-effort cleanup only */ }
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/** The caps a headless run may set without code: `PI_TYPESAFE_MAX_USD_PER_DAY` and its siblings. */
|
|
84
|
+
export function capsFromEnvironment(env = process.env) {
|
|
85
|
+
const number = (name) => {
|
|
86
|
+
const raw = env[name]?.trim();
|
|
87
|
+
if (!raw)
|
|
88
|
+
return undefined;
|
|
89
|
+
const value = Number(raw);
|
|
90
|
+
return Number.isFinite(value) && value > 0 ? value : undefined;
|
|
91
|
+
};
|
|
92
|
+
const maxRequestsPerDay = number("PI_TYPESAFE_MAX_REQUESTS_PER_DAY");
|
|
93
|
+
const maxInputTokensPerDay = number("PI_TYPESAFE_MAX_INPUT_TOKENS_PER_DAY");
|
|
94
|
+
const maxUsdPerDay = number("PI_TYPESAFE_MAX_USD_PER_DAY");
|
|
95
|
+
return {
|
|
96
|
+
...(maxRequestsPerDay === undefined ? {} : { maxRequestsPerDay: Math.floor(maxRequestsPerDay) }),
|
|
97
|
+
...(maxInputTokensPerDay === undefined ? {} : { maxInputTokensPerDay: Math.floor(maxInputTokensPerDay) }),
|
|
98
|
+
...(maxUsdPerDay === undefined ? {} : { maxUsdPerDay }),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
/** Environment first for day caps, then the explicit option; a cap is never raised by the environment. */
|
|
102
|
+
export function mergeCaps(explicit, environment) {
|
|
103
|
+
const lowest = (a, b) => a === undefined ? b : b === undefined ? a : Math.min(a, b);
|
|
104
|
+
const maxRequestsPerDay = lowest(explicit.maxRequestsPerDay, environment.maxRequestsPerDay);
|
|
105
|
+
const maxInputTokensPerDay = lowest(explicit.maxInputTokensPerDay, environment.maxInputTokensPerDay);
|
|
106
|
+
const maxUsdPerDay = lowest(explicit.maxUsdPerDay, environment.maxUsdPerDay);
|
|
107
|
+
return {
|
|
108
|
+
...(explicit.maxRequests === undefined ? {} : { maxRequests: explicit.maxRequests }),
|
|
109
|
+
...(maxRequestsPerDay === undefined ? {} : { maxRequestsPerDay }),
|
|
110
|
+
...(maxInputTokensPerDay === undefined ? {} : { maxInputTokensPerDay }),
|
|
111
|
+
...(maxUsdPerDay === undefined ? {} : { maxUsdPerDay }),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
export function openUsageLedger(options = {}) {
|
|
115
|
+
const path = options.path ?? usagePath();
|
|
116
|
+
const now = options.now ?? (() => new Date());
|
|
117
|
+
const usdPerMTok = options.usdPerMTok && options.usdPerMTok > 0 ? options.usdPerMTok : DEFAULT_USD_PER_MTOK;
|
|
118
|
+
let day = localDay(now());
|
|
119
|
+
let days = keepRecent(readDays(path), day);
|
|
120
|
+
let totals = days[day];
|
|
121
|
+
const report = (value, name) => ({
|
|
122
|
+
...value, day: name, estimatedUsd: estimateUsd(value.inputTokens, usdPerMTok),
|
|
123
|
+
});
|
|
124
|
+
const save = () => {
|
|
125
|
+
days = keepRecent({ ...days, [day]: totals }, day);
|
|
126
|
+
writeDays(path, days);
|
|
127
|
+
};
|
|
128
|
+
const roll = () => {
|
|
129
|
+
const current = localDay(now());
|
|
130
|
+
if (current === day)
|
|
131
|
+
return;
|
|
132
|
+
day = current;
|
|
133
|
+
totals = days[day] ?? emptyTotals();
|
|
134
|
+
days = keepRecent(days, day);
|
|
135
|
+
};
|
|
136
|
+
const add = (delta) => {
|
|
137
|
+
roll();
|
|
138
|
+
totals = {
|
|
139
|
+
requestsStarted: totals.requestsStarted + (delta.requestsStarted ?? 0),
|
|
140
|
+
requestsSucceeded: totals.requestsSucceeded + (delta.requestsSucceeded ?? 0),
|
|
141
|
+
requestsFailed: totals.requestsFailed + (delta.requestsFailed ?? 0),
|
|
142
|
+
inputTokens: totals.inputTokens + (delta.inputTokens ?? 0),
|
|
143
|
+
outputTokens: totals.outputTokens + (delta.outputTokens ?? 0),
|
|
144
|
+
};
|
|
145
|
+
save();
|
|
146
|
+
};
|
|
147
|
+
return {
|
|
148
|
+
path,
|
|
149
|
+
usdPerMTok,
|
|
150
|
+
today: () => { roll(); return report(totals, day); },
|
|
151
|
+
recordStart: () => add({ requestsStarted: 1 }),
|
|
152
|
+
recordSuccess: (inputTokens, outputTokens) => add({
|
|
153
|
+
requestsSucceeded: 1,
|
|
154
|
+
inputTokens: Number.isSafeInteger(inputTokens) && inputTokens > 0 ? inputTokens : 0,
|
|
155
|
+
outputTokens: Number.isSafeInteger(outputTokens) && outputTokens > 0 ? outputTokens : 0,
|
|
156
|
+
}),
|
|
157
|
+
recordFailure: () => add({ requestsFailed: 1 }),
|
|
158
|
+
blocked: (caps) => {
|
|
159
|
+
roll();
|
|
160
|
+
const checks = [
|
|
161
|
+
["requestsPerDay", caps.maxRequestsPerDay, totals.requestsStarted],
|
|
162
|
+
["inputTokensPerDay", caps.maxInputTokensPerDay, totals.inputTokens],
|
|
163
|
+
["usdPerDay", caps.maxUsdPerDay, estimateUsd(totals.inputTokens, usdPerMTok)],
|
|
164
|
+
];
|
|
165
|
+
for (const [cap, limit, used] of checks) {
|
|
166
|
+
if (limit !== undefined && used >= limit)
|
|
167
|
+
return { cap, limit, used, day };
|
|
168
|
+
}
|
|
169
|
+
return undefined;
|
|
170
|
+
},
|
|
171
|
+
describe: (caps = {}) => {
|
|
172
|
+
roll();
|
|
173
|
+
const current = report(totals, day);
|
|
174
|
+
const limits = [
|
|
175
|
+
caps.maxRequestsPerDay === undefined ? undefined : `${current.requestsStarted}/${caps.maxRequestsPerDay} requests`,
|
|
176
|
+
caps.maxInputTokensPerDay === undefined ? undefined : `${current.inputTokens}/${caps.maxInputTokensPerDay} input tokens`,
|
|
177
|
+
caps.maxUsdPerDay === undefined ? undefined : `$${current.estimatedUsd.toFixed(4)}/$${caps.maxUsdPerDay.toFixed(2)}`,
|
|
178
|
+
].filter((part) => part !== undefined);
|
|
179
|
+
return `${current.requestsStarted} requests today (${current.requestsSucceeded} ok, ${current.requestsFailed} failed), ${current.inputTokens} input / ${current.outputTokens} output tokens, ~$${current.estimatedUsd.toFixed(4)}${limits.length ? `; caps ${limits.join(", ")}` : "; no daily cap"}`;
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { ask, authState, createTypeSafe, choice, describeAuth, noul } from "pi-typesafe";
|
|
3
|
+
|
|
4
|
+
// A separate extension using the public API, not pi-typesafe's private modules.
|
|
5
|
+
// It owns its own consent and request budget; it does not reuse /typesafe enable.
|
|
6
|
+
export default function decisionExample(pi: ExtensionAPI): void {
|
|
7
|
+
pi.registerCommand("decision-demo", {
|
|
8
|
+
description: "Send one synthetic, batched decision request to TypeSafe",
|
|
9
|
+
async handler(_args, ctx) {
|
|
10
|
+
if (!ctx.hasUI) return;
|
|
11
|
+
// Say which key is in effect before showing a data notice, so a missing one is not mistaken for consent.
|
|
12
|
+
const auth = describeAuth(authState());
|
|
13
|
+
if (auth.level === "error") {
|
|
14
|
+
ctx.ui.notify(auth.text, "warning");
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
if (!await ctx.ui.confirm("Send a TypeSafe request?", "This synthetic example goes to api.typesafe.ai and may incur charges.")) return;
|
|
18
|
+
try {
|
|
19
|
+
// One client instance, one session budget, and a daily spend cap the script cannot forget.
|
|
20
|
+
const client = createTypeSafe({ maxRequests: 1, maxUsdPerDay: 1 });
|
|
21
|
+
const answer = await ask(client, {
|
|
22
|
+
state: "Please refund the duplicate charge.",
|
|
23
|
+
questions: {
|
|
24
|
+
team: choice("Which team should handle the request?", {
|
|
25
|
+
billing: "Payments and refunds", engineering: "Software defects", other: "Anything else",
|
|
26
|
+
}),
|
|
27
|
+
refund: noul("Is the sender requesting a refund?"),
|
|
28
|
+
},
|
|
29
|
+
}, { timeoutMs: 5_000 });
|
|
30
|
+
if (!answer.ok) {
|
|
31
|
+
ctx.ui.notify(answer.errorCode === "budget" ? `No request was sent: ${answer.error}` : "The example could not complete.", "warning");
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
ctx.ui.notify(`Team: ${answer.answers.team.choice}; P(refund): ${answer.answers.refund.noul}; ${answer.elapsedMs} ms`, "info");
|
|
35
|
+
} catch (error) {
|
|
36
|
+
// createTypeSafe rejects an unusable key store; ask() never throws.
|
|
37
|
+
ctx.ui.notify(error instanceof Error ? error.message : "The example could not start.", "error");
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
}
|