@gonrocca/zero-pi 0.1.24 → 0.1.26
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 +10 -6
- package/extensions/provider-guard-extension.ts +54 -119
- package/extensions/provider-guard.ts +49 -134
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -14,9 +14,12 @@
|
|
|
14
14
|
**The zero spec-driven development workflow, packaged for [pi](https://pi.dev).**
|
|
15
15
|
|
|
16
16
|
[](https://www.npmjs.com/package/@gonrocca/zero-pi)
|
|
17
|
+
[](https://github.com/gonzalonicolasr/zero-pi)
|
|
17
18
|
[](./LICENSE)
|
|
18
19
|
[](https://nodejs.org)
|
|
19
20
|
|
|
21
|
+
**Repositorio:** [github.com/gonzalonicolasr/zero-pi](https://github.com/gonzalonicolasr/zero-pi)
|
|
22
|
+
|
|
20
23
|
</div>
|
|
21
24
|
|
|
22
25
|
---
|
|
@@ -195,12 +198,13 @@ re-derived.
|
|
|
195
198
|
|
|
196
199
|
### Provider guard — `provider-guard.ts`
|
|
197
200
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
per token from your metered extra-usage pool
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
201
|
+
The `anthropic` provider runs two ways: a Claude Pro/Max **subscription** login
|
|
202
|
+
(OAuth, via `/login` — `pi-claude-oauth-adapter` smooths this path) or an **API
|
|
203
|
+
key**, which bills per token from your metered extra-usage pool. Same provider
|
|
204
|
+
id, different billing. The guard watches model switches and reads pi's auth mode
|
|
205
|
+
(`modelRegistry.isUsingOAuth`): when a model runs on `anthropic` with an API key
|
|
206
|
+
it emits a single non-blocking warning suggesting `/login`. On OAuth, or on any
|
|
207
|
+
other provider, it stays silent.
|
|
204
208
|
|
|
205
209
|
### Startup banner — `startup-banner.ts`
|
|
206
210
|
|
|
@@ -1,166 +1,105 @@
|
|
|
1
|
-
// zero-pi — metered-
|
|
1
|
+
// zero-pi — Anthropic metered-billing guard, pi wiring.
|
|
2
2
|
//
|
|
3
3
|
// A thin pi extension that wires the pure decision logic in `provider-guard.ts`
|
|
4
|
-
// to the `model_select` hook.
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
// deliberate switch to a metered provider that has an equivalent — a `confirm`
|
|
9
|
-
// dialog offering to redirect to the subscription provider.
|
|
4
|
+
// to the `model_select` hook. On every model switch it reads the auth mode from
|
|
5
|
+
// `ctx.modelRegistry.isUsingOAuth` and, when the `anthropic` provider runs on an
|
|
6
|
+
// API key (metered extra usage) rather than a Claude Pro/Max subscription OAuth
|
|
7
|
+
// login, emits a single non-blocking `warning`.
|
|
10
8
|
//
|
|
11
|
-
// All decisions live in `provider-guard.ts`; this file
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
// `provider-guard.ts` is imported (no `node:*` is needed here, no third party).
|
|
9
|
+
// All decisions live in `provider-guard.ts`; this file declares the minimal
|
|
10
|
+
// pi-API surface it uses, hooks `model_select`, and performs the I/O (`notify`).
|
|
11
|
+
// Both `register` and the handler are wrapped in a swallowing `try/catch` — a
|
|
12
|
+
// failure must never break a pi session. Dependency-free: only
|
|
13
|
+
// `provider-guard.ts` is imported (no `node:*`, no third party).
|
|
17
14
|
|
|
18
|
-
import {
|
|
19
|
-
classifyModelSwitch,
|
|
20
|
-
redirectFailedMessage,
|
|
21
|
-
type ModelLike,
|
|
22
|
-
} from "./provider-guard.ts";
|
|
15
|
+
import { classifyModelSwitch } from "./provider-guard.ts";
|
|
23
16
|
|
|
24
17
|
// ---------------------------------------------------------------------------
|
|
25
18
|
// Minimal local pi-API interfaces
|
|
26
19
|
// ---------------------------------------------------------------------------
|
|
27
|
-
//
|
|
28
|
-
// `provider-guard-extension.ts` declares only the slice of pi's API it uses,
|
|
29
|
-
// exactly like `autotune-extension.ts` and `zero-models.ts`. The guard never
|
|
30
|
-
// depends on pi's full type surface — just the shapes below.
|
|
31
20
|
|
|
32
|
-
/** The minimum a `Model` of pi must expose for the guard.
|
|
33
|
-
* of the pure module — `setModel`/the registry both speak this shape. */
|
|
21
|
+
/** The minimum a `Model` of pi must expose for the guard. */
|
|
34
22
|
interface PiModel {
|
|
35
23
|
provider: string;
|
|
36
24
|
id: string;
|
|
37
25
|
}
|
|
38
26
|
|
|
39
|
-
/**
|
|
40
|
-
*
|
|
27
|
+
/**
|
|
28
|
+
* The model-registry slice the guard uses. `isUsingOAuth` reports whether a
|
|
29
|
+
* model authenticates through a Claude subscription OAuth login rather than an
|
|
30
|
+
* API key. It is optional — older pi builds may not expose it (see
|
|
31
|
+
* `resolveOAuth`).
|
|
32
|
+
*/
|
|
41
33
|
interface PiModelRegistry {
|
|
42
|
-
|
|
34
|
+
isUsingOAuth?(model: PiModel): boolean;
|
|
43
35
|
}
|
|
44
36
|
|
|
45
|
-
/** The slice of pi's UI surface the guard uses.
|
|
46
|
-
* (pi versions differ); both are handled uniformly with `await`. */
|
|
37
|
+
/** The slice of pi's UI surface the guard uses. */
|
|
47
38
|
interface PiUI {
|
|
48
|
-
confirm(title: string, message: string): Promise<boolean> | boolean;
|
|
49
39
|
notify(message: string, type?: "info" | "warning" | "error"): void;
|
|
50
40
|
}
|
|
51
41
|
|
|
52
|
-
/** The context pi passes to a `model_select` handler.
|
|
53
|
-
* optional — if absent the guard degrades to warn-only (it can no longer
|
|
54
|
-
* resolve equivalents) rather than breaking. */
|
|
42
|
+
/** The context pi passes to a `model_select` handler. */
|
|
55
43
|
interface PiModelSelectContext {
|
|
56
44
|
ui: PiUI;
|
|
57
45
|
modelRegistry?: PiModelRegistry;
|
|
58
46
|
}
|
|
59
47
|
|
|
60
|
-
/** The `model_select` event pi emits on a model change.
|
|
61
|
-
* `model` and `source`; `source` is `set`/`cycle`/`restore` or, defensively,
|
|
62
|
-
* any other string a future pi version might report. */
|
|
48
|
+
/** The `model_select` event pi emits on a model change. */
|
|
63
49
|
interface PiModelSelectEvent {
|
|
64
50
|
type: "model_select";
|
|
65
51
|
model?: PiModel;
|
|
66
|
-
source?: "set" | "cycle" | "restore" | string;
|
|
67
52
|
}
|
|
68
53
|
|
|
69
|
-
/** The slice of pi's extension API the guard uses
|
|
70
|
-
* `setModel` to apply a redirection. */
|
|
54
|
+
/** The slice of pi's extension API the guard uses. */
|
|
71
55
|
interface PiExtensionAPI {
|
|
72
56
|
on(
|
|
73
57
|
event: string,
|
|
74
|
-
handler: (event: PiModelSelectEvent, ctx: PiModelSelectContext) => void
|
|
58
|
+
handler: (event: PiModelSelectEvent, ctx: PiModelSelectContext) => void,
|
|
75
59
|
): void;
|
|
76
|
-
setModel(model: PiModel): Promise<boolean>;
|
|
77
60
|
}
|
|
78
61
|
|
|
79
62
|
// ---------------------------------------------------------------------------
|
|
80
63
|
// Handler
|
|
81
64
|
// ---------------------------------------------------------------------------
|
|
82
65
|
|
|
66
|
+
/**
|
|
67
|
+
* Resolve whether `model` authenticates through subscription OAuth.
|
|
68
|
+
*
|
|
69
|
+
* Calls `modelRegistry.isUsingOAuth` when available. When the method is absent
|
|
70
|
+
* (older pi) or throws, it returns `true` — an unknown auth mode must never
|
|
71
|
+
* produce a false metered warning.
|
|
72
|
+
*/
|
|
73
|
+
function resolveOAuth(registry: PiModelRegistry | undefined, model: PiModel): boolean {
|
|
74
|
+
if (!registry || typeof registry.isUsingOAuth !== "function") return true;
|
|
75
|
+
try {
|
|
76
|
+
return registry.isUsingOAuth(model) !== false;
|
|
77
|
+
} catch {
|
|
78
|
+
// A registry failure degrades to "assume OAuth" — never a false warning.
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
83
|
/**
|
|
84
84
|
* The `model_select` guard handler.
|
|
85
85
|
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
* propagate out of a pi session. The flow:
|
|
86
|
+
* Wrapped in a swallowing `try/catch` so no failure can propagate out of a pi
|
|
87
|
+
* session. The flow:
|
|
89
88
|
*
|
|
90
|
-
* 1. Validate `event`/`event.model` — a malformed event
|
|
91
|
-
* 2.
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
* 3. `classifyModelSwitch(model, source, lookup)` → a `GuardAction`.
|
|
95
|
-
* 4. Execute the action: `ignore` → no-op; `warn` → `notify(..., "warning")`;
|
|
96
|
-
* `offer-redirect` → `confirm`, and on a truthy answer `setModel` +
|
|
97
|
-
* `notify(..., "info")` (or the failure notice if `setModel` returns
|
|
98
|
-
* `false`). A falsy `confirm` leaves the user on the metered provider.
|
|
89
|
+
* 1. Validate `event`/`event.model`/`ctx.ui` — a malformed event bails out.
|
|
90
|
+
* 2. Resolve the auth mode via `resolveOAuth`.
|
|
91
|
+
* 3. `classifyModelSwitch(model, isOAuth)` → a `GuardAction`.
|
|
92
|
+
* 4. `warn` → `notify(..., "warning")`; `ignore` → no-op.
|
|
99
93
|
*/
|
|
100
|
-
|
|
101
|
-
event: PiModelSelectEvent,
|
|
102
|
-
ctx: PiModelSelectContext,
|
|
103
|
-
pi: PiExtensionAPI,
|
|
104
|
-
): Promise<void> {
|
|
94
|
+
function handleModelSelect(event: PiModelSelectEvent, ctx: PiModelSelectContext): void {
|
|
105
95
|
try {
|
|
106
|
-
// 1. Malformed event — no event, no model, or no UI to talk to: bail out.
|
|
107
96
|
if (!event || !event.model || !ctx || !ctx.ui || typeof ctx.ui.notify !== "function") {
|
|
108
97
|
return;
|
|
109
98
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
// lookup, so `find` is wrapped in a `try/catch` returning `undefined`.
|
|
113
|
-
// If `modelRegistry` is absent the guard cannot resolve equivalents —
|
|
114
|
-
// degrade to an always-`undefined` lookup so a metered provider still
|
|
115
|
-
// produces a warning rather than breaking.
|
|
116
|
-
const registry = ctx.modelRegistry;
|
|
117
|
-
const lookup = (provider: string, id: string): ModelLike | undefined => {
|
|
118
|
-
if (!registry || typeof registry.find !== "function") return undefined;
|
|
119
|
-
try {
|
|
120
|
-
return registry.find(provider, id);
|
|
121
|
-
} catch {
|
|
122
|
-
// A registry lookup failure degrades to "no equivalent", never throws.
|
|
123
|
-
return undefined;
|
|
124
|
-
}
|
|
125
|
-
};
|
|
126
|
-
|
|
127
|
-
// 3. Classify the switch in the pure module.
|
|
128
|
-
const action = classifyModelSwitch(event.model, event.source, lookup);
|
|
129
|
-
|
|
130
|
-
// 4. Execute the action.
|
|
131
|
-
if (action.kind === "ignore") {
|
|
132
|
-
// Non-metered provider or malformed event — nothing to do.
|
|
133
|
-
return;
|
|
134
|
-
}
|
|
135
|
-
|
|
99
|
+
const isOAuth = resolveOAuth(ctx.modelRegistry, event.model);
|
|
100
|
+
const action = classifyModelSwitch(event.model, isOAuth);
|
|
136
101
|
if (action.kind === "warn") {
|
|
137
|
-
// Metered provider with no equivalent, or a session `restore` — a single
|
|
138
|
-
// non-blocking warning, no modal, no `setModel`.
|
|
139
102
|
ctx.ui.notify(action.message, "warning");
|
|
140
|
-
return;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// action.kind === "offer-redirect" — deliberate switch to a metered
|
|
144
|
-
// provider that has a subscription equivalent. Offer the redirect; the
|
|
145
|
-
// `confirm` dialog itself is the user's escape (AC 1.5).
|
|
146
|
-
const accepted = await ctx.ui.confirm(action.confirmTitle, action.confirmMessage);
|
|
147
|
-
if (!accepted) {
|
|
148
|
-
// User declined or cancelled — leave them on the metered provider, no
|
|
149
|
-
// second warning, no `setModel`.
|
|
150
|
-
return;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
const applied = await pi.setModel(action.safeModel);
|
|
154
|
-
if (applied) {
|
|
155
|
-
// Redirection took effect — confirm it with an `info` notification.
|
|
156
|
-
ctx.ui.notify(action.redirectMessage, "info");
|
|
157
|
-
} else {
|
|
158
|
-
// `setModel` returned `false`: the switch did not apply. Do not claim
|
|
159
|
-
// "redirected" — emit the optional failure notice instead.
|
|
160
|
-
ctx.ui.notify(
|
|
161
|
-
redirectFailedMessage(action.safeModel.id, action.safeModel.provider),
|
|
162
|
-
"warning",
|
|
163
|
-
);
|
|
164
103
|
}
|
|
165
104
|
} catch {
|
|
166
105
|
// Any failure of the guard must never break a pi session.
|
|
@@ -175,20 +114,16 @@ async function handleModelSelect(
|
|
|
175
114
|
* The pi extension entry point.
|
|
176
115
|
*
|
|
177
116
|
* pi calls this once when the extension loads. It wires the guard handler to
|
|
178
|
-
* the `model_select` event. The
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
* `pi` (missing, or no `.on` function), it no-ops cleanly.
|
|
117
|
+
* the `model_select` event. The body is wrapped in a swallowing `try/catch`,
|
|
118
|
+
* and the handler is wrapped again, so neither registration nor a later failure
|
|
119
|
+
* can break a pi session. Called with no or an invalid `pi`, it no-ops cleanly.
|
|
182
120
|
*/
|
|
183
121
|
export default function register(pi?: unknown): void {
|
|
184
122
|
try {
|
|
185
123
|
if (!pi || typeof (pi as PiExtensionAPI).on !== "function") {
|
|
186
124
|
return;
|
|
187
125
|
}
|
|
188
|
-
|
|
189
|
-
api.on("model_select", (event: PiModelSelectEvent, ctx: PiModelSelectContext): Promise<void> => {
|
|
190
|
-
return handleModelSelect(event, ctx, api);
|
|
191
|
-
});
|
|
126
|
+
(pi as PiExtensionAPI).on("model_select", handleModelSelect);
|
|
192
127
|
} catch {
|
|
193
128
|
// Registration itself must never break a pi session.
|
|
194
129
|
}
|
|
@@ -1,48 +1,26 @@
|
|
|
1
|
-
// zero-pi — metered-
|
|
1
|
+
// zero-pi — Anthropic metered-billing guard, pure-logic module.
|
|
2
2
|
//
|
|
3
|
-
// pi
|
|
4
|
-
//
|
|
5
|
-
// subscription PLAN — no per-token charge) and `anthropic` (the direct
|
|
6
|
-
// Anthropic API provider, which consumes a metered "extra usage" pool and
|
|
7
|
-
// bills per token). `/model` lists the same models under both, so it is easy
|
|
8
|
-
// to slide into metered billing unnoticed.
|
|
3
|
+
// pi reaches Claude models through the `anthropic` provider with one of two
|
|
4
|
+
// auth modes:
|
|
9
5
|
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
// it
|
|
13
|
-
// `
|
|
14
|
-
//
|
|
6
|
+
// • OAuth — a Claude Pro/Max subscription login (`/login`). The
|
|
7
|
+
// `pi-claude-oauth-adapter` package smooths this path. This is the mode
|
|
8
|
+
// to prefer: it authenticates against the Claude subscription.
|
|
9
|
+
// • API key — `ANTHROPIC_API_KEY`. This bills per token from the metered
|
|
10
|
+
// "extra usage" pool.
|
|
15
11
|
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
* metered↔subscription pair is a single extra line.
|
|
26
|
-
*
|
|
27
|
-
* Invariant the no-redirect-loop guarantee depends on: a redirection target
|
|
28
|
-
* (a *value*) must never itself be a *key*. `pi-claude-cli` is a value, not a
|
|
29
|
-
* key, so the `setModel`-triggered second `model_select` event classifies as
|
|
30
|
-
* `ignore` and the chain terminates.
|
|
31
|
-
*/
|
|
32
|
-
export const METERED_TO_SUBSCRIPTION: Readonly<Record<string, string>> = {
|
|
33
|
-
anthropic: "pi-claude-cli",
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Origin of a model change, as pi reports it on `ModelSelectEvent.source`.
|
|
38
|
-
* `set`/`cycle` are deliberate user actions; `restore` is a non-deliberate
|
|
39
|
-
* session restore that must never raise a modal.
|
|
40
|
-
*/
|
|
41
|
-
export type GuardSource = "set" | "cycle" | "restore";
|
|
12
|
+
// Same provider id (`anthropic`), different billing. pi reports the auth mode
|
|
13
|
+
// via `modelRegistry.isUsingOAuth(model)`, so the guard keys off that — not off
|
|
14
|
+
// the provider name. (The earlier guard redirected `anthropic` → `pi-claude-cli`;
|
|
15
|
+
// that provider is no longer used, so the redirect is gone.)
|
|
16
|
+
//
|
|
17
|
+
// This module holds the guard's decision in plain, dependency-free TypeScript
|
|
18
|
+
// so it is testable with plain objects via `node --test`. The pi wiring (the
|
|
19
|
+
// `model_select` handler that reads `isUsingOAuth` and calls `notify`) lives in
|
|
20
|
+
// `provider-guard-extension.ts`. No pi imports, no filesystem, no side effects.
|
|
42
21
|
|
|
43
|
-
/** The
|
|
44
|
-
|
|
45
|
-
const DELIBERATE_SOURCES: readonly string[] = ["set", "cycle"];
|
|
22
|
+
/** The pi provider whose billing depends on the auth mode. */
|
|
23
|
+
export const METERED_PROVIDER = "anthropic";
|
|
46
24
|
|
|
47
25
|
/** The minimum a `Model` of pi must expose for the guard to classify it. */
|
|
48
26
|
export interface ModelLike {
|
|
@@ -51,67 +29,24 @@ export interface ModelLike {
|
|
|
51
29
|
}
|
|
52
30
|
|
|
53
31
|
/**
|
|
54
|
-
*
|
|
55
|
-
* `ctx.modelRegistry.find`; the pure module never knows `modelRegistry` or any
|
|
56
|
-
* pi type. Returns `undefined` when no equivalent model exists. The wiring is
|
|
57
|
-
* responsible for wrapping `find` so this lookup is total — `classifyModelSwitch`
|
|
58
|
-
* assumes a `lookup` that never throws.
|
|
59
|
-
*/
|
|
60
|
-
export type RegistryLookup = (provider: string, id: string) => ModelLike | undefined;
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* The action the wiring must execute, as a discriminated union keyed on `kind`.
|
|
32
|
+
* The action the wiring must execute, a discriminated union keyed on `kind`.
|
|
64
33
|
*
|
|
65
|
-
* - `ignore` — no-op:
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
* - `offer-redirect` — show `ctx.ui.confirm(confirmTitle, confirmMessage)`; on
|
|
70
|
-
* a truthy answer `setModel(safeModel)` and, if applied, notify with
|
|
71
|
-
* `redirectMessage`.
|
|
34
|
+
* - `ignore` — no-op: not the `anthropic` provider, `anthropic` on OAuth
|
|
35
|
+
* (subscription), or a malformed event.
|
|
36
|
+
* - `warn` — emit `ctx.ui.notify(message, "warning")`: `anthropic` on an API
|
|
37
|
+
* key, which bills metered extra usage.
|
|
72
38
|
*/
|
|
73
39
|
export type GuardAction =
|
|
74
40
|
| { kind: "ignore" }
|
|
75
|
-
| { kind: "warn"; message: string }
|
|
76
|
-
| {
|
|
77
|
-
kind: "offer-redirect";
|
|
78
|
-
safeModel: ModelLike;
|
|
79
|
-
confirmTitle: string;
|
|
80
|
-
confirmMessage: string;
|
|
81
|
-
redirectMessage: string;
|
|
82
|
-
};
|
|
41
|
+
| { kind: "warn"; message: string };
|
|
83
42
|
|
|
84
43
|
// ---------------------------------------------------------------------------
|
|
85
|
-
// User-facing Spanish
|
|
44
|
+
// User-facing Spanish string
|
|
86
45
|
// ---------------------------------------------------------------------------
|
|
87
|
-
//
|
|
88
|
-
// Exported as constants/functions so the test can assert them verbatim. `id`
|
|
89
|
-
// is the model id (e.g. `claude-opus-4-7`); `metered`/`subscription` are the
|
|
90
|
-
// provider names from `METERED_TO_SUBSCRIPTION`.
|
|
91
|
-
|
|
92
|
-
/** Title of the redirect `confirm` dialog. */
|
|
93
|
-
export const CONFIRM_TITLE = "zero · proveedor con cobro por token";
|
|
94
|
-
|
|
95
|
-
/** Body of the redirect `confirm` dialog — explains the metered billing and
|
|
96
|
-
* asks whether to switch to the subscription equivalent. */
|
|
97
|
-
export function confirmMessage(id: string, metered: string, subscription: string): string {
|
|
98
|
-
return `Estás cambiando a «${id}» vía el proveedor «${metered}», que factura por token y consume tu pool medido de extra usage. ¿Querés cambiar al equivalente «${id}» en «${subscription}», que usa los límites de tu suscripción?`;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/** `info` notification emitted after `setModel` succeeds. */
|
|
102
|
-
export function redirectMessage(id: string, subscription: string): string {
|
|
103
|
-
return `zero: redirigido a «${id}» en ${subscription} — usando los límites de tu suscripción.`;
|
|
104
|
-
}
|
|
105
46
|
|
|
106
|
-
/** `warning` notification —
|
|
107
|
-
export function warnMessage(id: string
|
|
108
|
-
return `zero: «${id}»
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/** `warning` notification — `setModel` returned `false` (redirection did not
|
|
112
|
-
* apply). Optional; emitted by the wiring as a defensive courtesy. */
|
|
113
|
-
export function redirectFailedMessage(id: string, subscription: string): string {
|
|
114
|
-
return `zero: no se pudo cambiar a «${id}» en ${subscription} — seguís en el proveedor medido.`;
|
|
47
|
+
/** `warning` notification — the model runs on the metered `anthropic` API key. */
|
|
48
|
+
export function warnMessage(id: string): string {
|
|
49
|
+
return `zero: «${id}» usa el provider «anthropic» con API key — factura por token (extra usage). Logueá con Claude Pro/Max vía /login para usar tu suscripción.`;
|
|
115
50
|
}
|
|
116
51
|
|
|
117
52
|
// ---------------------------------------------------------------------------
|
|
@@ -126,58 +61,38 @@ function isNonEmptyString(value: unknown): value is string {
|
|
|
126
61
|
/**
|
|
127
62
|
* Classify a model switch into the `GuardAction` the wiring must execute.
|
|
128
63
|
*
|
|
129
|
-
* Pure and total — never throws
|
|
130
|
-
*
|
|
64
|
+
* Pure and total — never throws. The branches, in order:
|
|
65
|
+
*
|
|
66
|
+
* 1. `model` falsy, or `provider`/`id` not non-empty strings → `ignore`
|
|
67
|
+
* (malformed event).
|
|
68
|
+
* 2. `model.provider` is not `anthropic` → `ignore` (other providers carry
|
|
69
|
+
* no OAuth-vs-API-key billing split this guard reasons about).
|
|
70
|
+
* 3. The provider is `anthropic`:
|
|
71
|
+
* - `isOAuth` truthy → `ignore` (subscription auth — the intended path).
|
|
72
|
+
* - `isOAuth` falsy → `warn` (API key — metered extra usage).
|
|
131
73
|
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
* (non-metered provider, AC 4.1/4.2 — this also classifies `pi-claude-cli`,
|
|
136
|
-
* which is the structural no-redirect-loop guarantee, Story 5).
|
|
137
|
-
* 3. The provider is metered. Resolve the subscription provider and call
|
|
138
|
-
* `lookup(subProvider, model.id)`:
|
|
139
|
-
* - `source === "restore"` → always `warn`, never a modal, regardless of
|
|
140
|
-
* whether an equivalent exists (AC 3.x).
|
|
141
|
-
* - `source` deliberate (`set`/`cycle`):
|
|
142
|
-
* - `lookup` found a model → `offer-redirect` (AC 1).
|
|
143
|
-
* - `lookup` returned `undefined` → `warn` (AC 2).
|
|
144
|
-
* - any other (unknown) `source` → treated as non-deliberate → `warn`
|
|
145
|
-
* (defensive: no modal unless the switch is known to be deliberate).
|
|
74
|
+
* `isOAuth` is supplied by the wiring from `modelRegistry.isUsingOAuth`. When
|
|
75
|
+
* the wiring cannot determine the auth mode it passes `true`, so an unknown
|
|
76
|
+
* state never produces a false warning.
|
|
146
77
|
*/
|
|
147
78
|
export function classifyModelSwitch(
|
|
148
79
|
model: ModelLike | null | undefined,
|
|
149
|
-
|
|
150
|
-
lookup: RegistryLookup,
|
|
80
|
+
isOAuth: boolean,
|
|
151
81
|
): GuardAction {
|
|
152
82
|
// 1. Malformed event — no model, or missing/empty provider/id.
|
|
153
83
|
if (!model || !isNonEmptyString(model.provider) || !isNonEmptyString(model.id)) {
|
|
154
84
|
return { kind: "ignore" };
|
|
155
85
|
}
|
|
156
86
|
|
|
157
|
-
// 2.
|
|
158
|
-
|
|
159
|
-
if (subscriptionProvider === undefined) {
|
|
87
|
+
// 2. Any provider other than `anthropic` — out of scope, silent no-op.
|
|
88
|
+
if (model.provider !== METERED_PROVIDER) {
|
|
160
89
|
return { kind: "ignore" };
|
|
161
90
|
}
|
|
162
91
|
|
|
163
|
-
// 3.
|
|
164
|
-
//
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
const isDeliberate = typeof source === "string" && DELIBERATE_SOURCES.includes(source);
|
|
168
|
-
|
|
169
|
-
if (isDeliberate) {
|
|
170
|
-
const safeModel = lookup(subscriptionProvider, id);
|
|
171
|
-
if (safeModel) {
|
|
172
|
-
return {
|
|
173
|
-
kind: "offer-redirect",
|
|
174
|
-
safeModel,
|
|
175
|
-
confirmTitle: CONFIRM_TITLE,
|
|
176
|
-
confirmMessage: confirmMessage(id, meteredProvider, subscriptionProvider),
|
|
177
|
-
redirectMessage: redirectMessage(id, subscriptionProvider),
|
|
178
|
-
};
|
|
179
|
-
}
|
|
92
|
+
// 3. `anthropic`: OAuth is the subscription path (fine); an API key is the
|
|
93
|
+
// metered path (warn).
|
|
94
|
+
if (isOAuth) {
|
|
95
|
+
return { kind: "ignore" };
|
|
180
96
|
}
|
|
181
|
-
|
|
182
|
-
return { kind: "warn", message: warnMessage(id, meteredProvider) };
|
|
97
|
+
return { kind: "warn", message: warnMessage(model.id) };
|
|
183
98
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gonrocca/zero-pi",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.26",
|
|
4
4
|
"description": "zero-pi — an installable layer for pi (pi.dev): the zero spec-driven development workflow, skill auto-learning, and an animated ZERO startup banner. Adds capability to pi without modifying pi.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|