hanzoai 8.5.621 → 8.5.640
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 +64 -9
- package/dist/answer.js +20 -10
- package/dist/api/ai-api.d.ts +280 -14
- package/dist/api/ai-api.js +315 -29
- package/dist/api/auto-api.d.ts +64 -0
- package/dist/api/auto-api.js +77 -0
- package/dist/api/billing-api.d.ts +4 -4
- package/dist/api/billing-api.js +4 -4
- package/dist/api/git-api.d.ts +30 -0
- package/dist/api/git-api.js +59 -0
- package/dist/api/task-api.d.ts +55 -0
- package/dist/api/task-api.js +71 -0
- package/dist/client.d.ts +3 -2
- package/dist/client.js +4 -3
- package/dist/esm/answer.js +20 -10
- package/dist/esm/api/ai-api.d.ts +280 -14
- package/dist/esm/api/ai-api.js +315 -29
- package/dist/esm/api/auto-api.d.ts +64 -0
- package/dist/esm/api/auto-api.js +77 -0
- package/dist/esm/api/billing-api.d.ts +4 -4
- package/dist/esm/api/billing-api.js +4 -4
- package/dist/esm/api/git-api.d.ts +30 -0
- package/dist/esm/api/git-api.js +59 -0
- package/dist/esm/api/task-api.d.ts +55 -0
- package/dist/esm/api/task-api.js +71 -0
- package/dist/esm/client.d.ts +3 -2
- package/dist/esm/client.js +3 -2
- package/dist/esm/hanzo.d.ts +3 -0
- package/dist/esm/hanzo.js +6 -0
- package/dist/esm/models/agent-target-view.d.ts +6 -0
- package/dist/esm/models/ai-chat-in.d.ts +42 -0
- package/dist/esm/models/ai-chat.d.ts +37 -0
- package/dist/esm/models/ai-decide-in.d.ts +50 -0
- package/dist/esm/models/ai-decide-in.js +14 -0
- package/dist/esm/models/ai-decision-sides.d.ts +4 -5
- package/dist/esm/models/{ai-decision-sides-false.d.ts → ai-decision.d.ts} +15 -2
- package/dist/esm/models/ai-decision.js +14 -0
- package/dist/esm/models/ai-decisions-answer.d.ts +2 -3
- package/dist/esm/models/ai-decisions-choice.d.ts +4 -8
- package/dist/esm/models/ai-decisions-noul.d.ts +2 -3
- package/dist/esm/models/ai-decisions-request.d.ts +2 -3
- package/dist/esm/models/ai-decisions-score.d.ts +4 -5
- package/dist/esm/models/ai-model.d.ts +73 -0
- package/dist/esm/models/ai-model.js +14 -0
- package/dist/esm/models/{ai-decisions-choice-criteria-value.d.ts → ai-models.d.ts} +15 -2
- package/dist/esm/models/ai-models.js +14 -0
- package/dist/esm/models/ai-price.d.ts +48 -0
- package/dist/esm/models/ai-price.js +14 -0
- package/dist/esm/models/ai-receipt.d.ts +60 -0
- package/dist/esm/models/ai-receipt.js +14 -0
- package/dist/esm/models/ai-routing-reward-request.d.ts +54 -0
- package/dist/esm/models/ai-routing-reward-request.js +24 -0
- package/dist/esm/models/auto-automation-in.d.ts +6 -0
- package/dist/esm/models/auto-automation-patch.d.ts +6 -0
- package/dist/esm/models/auto-automation-run.d.ts +14 -1
- package/dist/esm/models/auto-automation.d.ts +6 -0
- package/dist/esm/models/auto-post-result.d.ts +36 -0
- package/dist/esm/models/auto-post-result.js +14 -0
- package/dist/esm/models/auto-review-in.d.ts +36 -0
- package/dist/esm/models/auto-review-in.js +14 -0
- package/dist/esm/models/git-workspace-view.d.ts +36 -0
- package/dist/esm/models/git-workspace-view.js +14 -0
- package/dist/esm/models/iam-user.d.ts +1 -1
- package/dist/esm/models/index.d.ts +14 -2
- package/dist/esm/models/index.js +14 -2
- package/dist/esm/models/provider-github-import-out.d.ts +6 -0
- package/dist/esm/models/provider-github-repo-item.d.ts +6 -0
- package/dist/esm/models/task-comment-list.d.ts +31 -0
- package/dist/esm/models/task-comment-list.js +14 -0
- package/dist/esm/models/task-comment.d.ts +42 -0
- package/dist/esm/models/task-comment.js +14 -0
- package/dist/esm/transport.d.ts +4 -0
- package/dist/esm/transport.js +188 -0
- package/dist/esm/usage.d.ts +98 -0
- package/dist/esm/usage.js +161 -0
- package/dist/hanzo.d.ts +3 -0
- package/dist/hanzo.js +16 -1
- package/dist/models/agent-target-view.d.ts +6 -0
- package/dist/models/ai-chat-in.d.ts +42 -0
- package/dist/models/ai-chat.d.ts +37 -0
- package/dist/models/ai-decide-in.d.ts +50 -0
- package/dist/models/ai-decide-in.js +15 -0
- package/dist/models/ai-decision-sides.d.ts +4 -5
- package/dist/models/{ai-decision-sides-false.d.ts → ai-decision.d.ts} +15 -2
- package/dist/models/ai-decision.js +15 -0
- package/dist/models/ai-decisions-answer.d.ts +2 -3
- package/dist/models/ai-decisions-choice.d.ts +4 -8
- package/dist/models/ai-decisions-noul.d.ts +2 -3
- package/dist/models/ai-decisions-request.d.ts +2 -3
- package/dist/models/ai-decisions-score.d.ts +4 -5
- package/dist/models/ai-model.d.ts +73 -0
- package/dist/models/ai-model.js +15 -0
- package/dist/models/{ai-decisions-choice-criteria-value.d.ts → ai-models.d.ts} +15 -2
- package/dist/models/ai-models.js +15 -0
- package/dist/models/ai-price.d.ts +48 -0
- package/dist/models/ai-price.js +15 -0
- package/dist/models/ai-receipt.d.ts +60 -0
- package/dist/models/ai-receipt.js +15 -0
- package/dist/models/ai-routing-reward-request.d.ts +54 -0
- package/dist/models/ai-routing-reward-request.js +27 -0
- package/dist/models/auto-automation-in.d.ts +6 -0
- package/dist/models/auto-automation-patch.d.ts +6 -0
- package/dist/models/auto-automation-run.d.ts +14 -1
- package/dist/models/auto-automation.d.ts +6 -0
- package/dist/models/auto-post-result.d.ts +36 -0
- package/dist/models/auto-post-result.js +15 -0
- package/dist/models/auto-review-in.d.ts +36 -0
- package/dist/models/auto-review-in.js +15 -0
- package/dist/models/git-workspace-view.d.ts +36 -0
- package/dist/models/git-workspace-view.js +15 -0
- package/dist/models/iam-user.d.ts +1 -1
- package/dist/models/index.d.ts +14 -2
- package/dist/models/index.js +14 -2
- package/dist/models/provider-github-import-out.d.ts +6 -0
- package/dist/models/provider-github-repo-item.d.ts +6 -0
- package/dist/models/task-comment-list.d.ts +31 -0
- package/dist/models/task-comment-list.js +15 -0
- package/dist/models/task-comment.d.ts +42 -0
- package/dist/models/task-comment.js +15 -0
- package/dist/transport.d.ts +4 -0
- package/dist/transport.js +195 -0
- package/dist/usage.d.ts +98 -0
- package/dist/usage.js +174 -0
- package/package.json +1 -1
- package/src/answer.ts +23 -10
- package/src/api/ai-api.ts +503 -29
- package/src/api/auto-api.ts +117 -0
- package/src/api/billing-api.ts +4 -4
- package/src/api/git-api.ts +68 -0
- package/src/api/task-api.ts +101 -0
- package/src/client.ts +3 -2
- package/src/hanzo.ts +18 -0
- package/src/models/agent-target-view.ts +6 -0
- package/src/models/ai-chat-in.ts +48 -0
- package/src/models/ai-chat.ts +45 -0
- package/src/models/ai-decide-in.ts +54 -0
- package/src/models/ai-decision-sides.ts +4 -7
- package/src/models/{ai-decision-sides-false.ts → ai-decision.ts} +17 -2
- package/src/models/ai-decisions-answer.ts +2 -5
- package/src/models/ai-decisions-choice.ts +4 -10
- package/src/models/ai-decisions-noul.ts +2 -5
- package/src/models/ai-decisions-question.ts +0 -3
- package/src/models/ai-decisions-request.ts +2 -5
- package/src/models/ai-decisions-score.ts +4 -7
- package/src/models/ai-model.ts +81 -0
- package/src/models/{ai-decisions-choice-criteria-value.ts → ai-models.ts} +17 -2
- package/src/models/ai-price.ts +54 -0
- package/src/models/ai-receipt.ts +66 -0
- package/src/models/ai-routing-reward-request.ts +63 -0
- package/src/models/auto-automation-in.ts +6 -0
- package/src/models/auto-automation-patch.ts +6 -0
- package/src/models/auto-automation-run.ts +16 -1
- package/src/models/auto-automation.ts +6 -0
- package/src/models/auto-post-result.ts +42 -0
- package/src/models/auto-review-in.ts +42 -0
- package/src/models/git-workspace-view.ts +42 -0
- package/src/models/iam-user.ts +1 -1
- package/src/models/index.ts +14 -2
- package/src/models/provider-github-import-out.ts +6 -0
- package/src/models/provider-github-repo-item.ts +6 -0
- package/src/models/task-comment-list.ts +39 -0
- package/src/models/task-comment.ts +48 -0
- package/src/transport.ts +190 -0
- package/src/usage.ts +203 -0
- /package/dist/esm/models/{ai-decision-sides-false.js → ai-chat-in.js} +0 -0
- /package/dist/esm/models/{ai-decisions-choice-criteria-value.js → ai-chat.js} +0 -0
- /package/dist/models/{ai-decision-sides-false.js → ai-chat-in.js} +0 -0
- /package/dist/models/{ai-decisions-choice-criteria-value.js → ai-chat.js} +0 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// transport — the Configuration this package exports.
|
|
3
|
+
//
|
|
4
|
+
// It is the generated Configuration plus one thing: a request a generated *Api
|
|
5
|
+
// class sends with it that comes back 402 or 429 with a usage refusal in the
|
|
6
|
+
// body throws that refusal's typed error (usage.ts). Every other answer passes
|
|
7
|
+
// through untouched — a 400 is still the AxiosError it was.
|
|
8
|
+
//
|
|
9
|
+
// The error path is axios's own: one response interceptor on the axios instance
|
|
10
|
+
// the generated classes use by default, installed by the first Configuration
|
|
11
|
+
// built. It acts only on requests marked as this package's, and the mark rides
|
|
12
|
+
// in `baseOptions`, which the generated client spreads into every request it
|
|
13
|
+
// builds — so the caller's other axios traffic, adapters and transforms are left
|
|
14
|
+
// as they were. An axios instance passed as an *Api's third argument has
|
|
15
|
+
// interceptors of its own, and a refusal through it stays an AxiosError.
|
|
16
|
+
//
|
|
17
|
+
// hanzo.ts exports this class under the generated one's name, so
|
|
18
|
+
// `new Configuration({ accessToken })` from 'hanzoai' is this one, and so is
|
|
19
|
+
// `Client.configuration`.
|
|
20
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
21
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
22
|
+
};
|
|
23
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
24
|
+
exports.Configuration = void 0;
|
|
25
|
+
const axios_1 = __importDefault(require("axios"));
|
|
26
|
+
const configuration_1 = require("./configuration");
|
|
27
|
+
const usage_1 = require("./usage");
|
|
28
|
+
/**
|
|
29
|
+
* The request option that marks a request as sent with this Configuration. Its
|
|
30
|
+
* value is this module's own interceptor, so two copies of the package sharing
|
|
31
|
+
* one axios each type only their own requests, with their own classes.
|
|
32
|
+
*/
|
|
33
|
+
const MARK = 'hanzoUsage';
|
|
34
|
+
/** A stream body is read this far at most; a refusal is a few hundred bytes of JSON. */
|
|
35
|
+
const CAP = 1 << 16;
|
|
36
|
+
class Configuration extends configuration_1.Configuration {
|
|
37
|
+
constructor(param = {}) {
|
|
38
|
+
super(param);
|
|
39
|
+
this.baseOptions = { ...this.baseOptions, [MARK]: refused };
|
|
40
|
+
// Checked, not remembered: a test teardown's `interceptors.response.clear()`
|
|
41
|
+
// removes it, and the next Configuration puts it back.
|
|
42
|
+
const installed = axios_1.default.interceptors.response.handlers?.some((h) => h?.rejected === refused);
|
|
43
|
+
if (!installed)
|
|
44
|
+
axios_1.default.interceptors.response.use(undefined, refused);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
exports.Configuration = Configuration;
|
|
48
|
+
/** Rethrow a marked 402 or 429 as the usage refusal its body names; anything else as it came. */
|
|
49
|
+
async function refused(err) {
|
|
50
|
+
const e = err;
|
|
51
|
+
const res = e?.response;
|
|
52
|
+
const config = e?.config;
|
|
53
|
+
if (!res || config?.[MARK] !== refused || (res.status !== 402 && res.status !== 429))
|
|
54
|
+
throw err;
|
|
55
|
+
let parsed;
|
|
56
|
+
try {
|
|
57
|
+
parsed = await body(res, config);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
// A body that cannot be read names nothing: the answer is the one axios gave.
|
|
61
|
+
throw err;
|
|
62
|
+
}
|
|
63
|
+
throw (0, usage_1.limited)(res.status, parsed, (0, usage_1.header)(res.headers, 'x-request-id'), err) ?? err;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* The body as JSON where it is JSON. axios hands it over parsed by default and
|
|
67
|
+
* raw when the caller asked for a buffer, a blob or a stream. A stream is read
|
|
68
|
+
* only when it says it is JSON — a refusal is, an event stream never is — at
|
|
69
|
+
* most CAP bytes, no longer than the caller's signal and timeout allow, and
|
|
70
|
+
* without taking it from the caller: `res.data` is left holding a stream of the
|
|
71
|
+
* same family that yields every byte again.
|
|
72
|
+
*/
|
|
73
|
+
async function body(res, config) {
|
|
74
|
+
let data = res.data;
|
|
75
|
+
if (web(data) || node(data)) {
|
|
76
|
+
if (!/json/i.test((0, usage_1.header)(res.headers, 'content-type') ?? ''))
|
|
77
|
+
return undefined;
|
|
78
|
+
const read = web(data) ? peekWeb(data, config) : peekNode(data, config);
|
|
79
|
+
const { text, again } = await read;
|
|
80
|
+
res.data = again;
|
|
81
|
+
data = text;
|
|
82
|
+
}
|
|
83
|
+
else if (typeof Blob !== 'undefined' && data instanceof Blob) {
|
|
84
|
+
data = await data.text();
|
|
85
|
+
}
|
|
86
|
+
else if (data instanceof ArrayBuffer || ArrayBuffer.isView(data)) {
|
|
87
|
+
data = new TextDecoder().decode(data);
|
|
88
|
+
}
|
|
89
|
+
if (typeof data !== 'string')
|
|
90
|
+
return data;
|
|
91
|
+
try {
|
|
92
|
+
return JSON.parse(data);
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return data;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/** A WHATWG stream: the fetch and xhr adapters' `responseType: 'stream'`. */
|
|
99
|
+
const web = (v) => typeof v?.tee === 'function' && typeof v.getReader === 'function';
|
|
100
|
+
const node = (v) => typeof v?.pipe === 'function' &&
|
|
101
|
+
typeof v[Symbol.asyncIterator] === 'function' &&
|
|
102
|
+
typeof v.constructor?.from === 'function';
|
|
103
|
+
/** Read a WHATWG stream through `peek`, and hand back a WHATWG stream that replays it. */
|
|
104
|
+
async function peekWeb(stream, config) {
|
|
105
|
+
const reader = stream.getReader();
|
|
106
|
+
const { text, rest } = await peek(() => reader.read(), config);
|
|
107
|
+
const again = new ReadableStream({
|
|
108
|
+
async pull(controller) {
|
|
109
|
+
const step = await rest.next();
|
|
110
|
+
if (step.done)
|
|
111
|
+
controller.close();
|
|
112
|
+
else
|
|
113
|
+
controller.enqueue(step.value);
|
|
114
|
+
},
|
|
115
|
+
cancel: (reason) => reader.cancel(reason),
|
|
116
|
+
});
|
|
117
|
+
return { text, again };
|
|
118
|
+
}
|
|
119
|
+
/** Read a Node stream through `peek`; `Readable.from` comes off its own constructor, so no bundle gains node:stream. */
|
|
120
|
+
async function peekNode(stream, config) {
|
|
121
|
+
const it = stream[Symbol.asyncIterator]();
|
|
122
|
+
const { text, rest } = await peek(() => it.next(), config);
|
|
123
|
+
return { text, again: stream.constructor.from(rest) };
|
|
124
|
+
}
|
|
125
|
+
const STOP = Symbol('stop');
|
|
126
|
+
/**
|
|
127
|
+
* Read chunks until the end, CAP bytes, the caller's abort or its timeout,
|
|
128
|
+
* whichever comes first. `text` is the whole body only when the end was
|
|
129
|
+
* reached — anything less names no refusal. `rest` yields every chunk read and
|
|
130
|
+
* then whatever is left, including a read still in flight and a failure, so the
|
|
131
|
+
* caller's stream loses nothing.
|
|
132
|
+
*/
|
|
133
|
+
async function peek(next, config) {
|
|
134
|
+
const seen = [];
|
|
135
|
+
const decoder = new TextDecoder();
|
|
136
|
+
let text = '';
|
|
137
|
+
let done = false;
|
|
138
|
+
let failed;
|
|
139
|
+
let pending;
|
|
140
|
+
let timer;
|
|
141
|
+
let quit;
|
|
142
|
+
const stop = new Promise((resolve) => {
|
|
143
|
+
quit = () => resolve(STOP);
|
|
144
|
+
const signal = config.signal;
|
|
145
|
+
if (signal && typeof signal.addEventListener === 'function') {
|
|
146
|
+
if (signal.aborted)
|
|
147
|
+
resolve(STOP);
|
|
148
|
+
signal.addEventListener('abort', () => resolve(STOP), { once: true });
|
|
149
|
+
}
|
|
150
|
+
if (config.timeout && config.timeout > 0)
|
|
151
|
+
timer = setTimeout(() => resolve(STOP), config.timeout);
|
|
152
|
+
});
|
|
153
|
+
try {
|
|
154
|
+
while (text.length < CAP) {
|
|
155
|
+
pending = next();
|
|
156
|
+
const step = await Promise.race([pending, stop]);
|
|
157
|
+
if (step === STOP)
|
|
158
|
+
break;
|
|
159
|
+
pending = undefined;
|
|
160
|
+
if (step.done) {
|
|
161
|
+
done = true;
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
seen.push(step.value);
|
|
165
|
+
text += typeof step.value === 'string' ? step.value : decoder.decode(step.value, { stream: true });
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
catch (error) {
|
|
169
|
+
pending = undefined;
|
|
170
|
+
failed = { error };
|
|
171
|
+
}
|
|
172
|
+
finally {
|
|
173
|
+
if (timer)
|
|
174
|
+
clearTimeout(timer);
|
|
175
|
+
quit?.();
|
|
176
|
+
}
|
|
177
|
+
async function* rest() {
|
|
178
|
+
yield* seen;
|
|
179
|
+
if (failed)
|
|
180
|
+
throw failed.error;
|
|
181
|
+
if (pending) {
|
|
182
|
+
const step = await pending;
|
|
183
|
+
if (step.done)
|
|
184
|
+
return;
|
|
185
|
+
yield step.value;
|
|
186
|
+
}
|
|
187
|
+
while (!done) {
|
|
188
|
+
const step = await next();
|
|
189
|
+
if (step.done)
|
|
190
|
+
return;
|
|
191
|
+
yield step.value;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return { text: done ? text : undefined, rest: rest() };
|
|
195
|
+
}
|
package/dist/usage.d.ts
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { type Obj } from './read';
|
|
2
|
+
/** One way out of a usage refusal, in the order to offer them. */
|
|
3
|
+
export interface UsageAction {
|
|
4
|
+
/** upgrade | switch | credits | topup. */
|
|
5
|
+
kind: string;
|
|
6
|
+
/** The words to put on the button. */
|
|
7
|
+
label?: string;
|
|
8
|
+
/** Where to go: the pay page, or `/v1/ai/limits` for "continue with credits". */
|
|
9
|
+
url?: string;
|
|
10
|
+
/** The plan an `upgrade` moves to. */
|
|
11
|
+
plan?: string;
|
|
12
|
+
/** The model a `switch` moves to. */
|
|
13
|
+
model?: string;
|
|
14
|
+
}
|
|
15
|
+
/** A priced request refused because nothing the caller holds may pay for it. */
|
|
16
|
+
export declare class UsageLimitError extends Error {
|
|
17
|
+
name: string;
|
|
18
|
+
/** 402 or 429. */
|
|
19
|
+
readonly status: number;
|
|
20
|
+
/** The machine code — one per subclass. */
|
|
21
|
+
readonly code: string;
|
|
22
|
+
/** `billing_error` or `rate_limit_error`. */
|
|
23
|
+
readonly type: string | undefined;
|
|
24
|
+
/** The class of usage refused: premium, ours or free. */
|
|
25
|
+
readonly usageClass: string | undefined;
|
|
26
|
+
/** The model the refusal is about. */
|
|
27
|
+
readonly model: string | undefined;
|
|
28
|
+
/** The model that answers instead, where there is one. */
|
|
29
|
+
readonly fallback: string | undefined;
|
|
30
|
+
/** When the refusal lifts by itself. */
|
|
31
|
+
readonly resetsAt: Date | undefined;
|
|
32
|
+
/** The upgrade page. */
|
|
33
|
+
readonly upgradeUrl: string | undefined;
|
|
34
|
+
readonly actions: UsageAction[];
|
|
35
|
+
/** `x-request-id` — the handle that finds this call in the audit trail. */
|
|
36
|
+
readonly request: string | undefined;
|
|
37
|
+
/** What the transport raised: the AxiosError on the generated client, nothing on the six. */
|
|
38
|
+
readonly cause: unknown;
|
|
39
|
+
constructor(status: number, error: Obj, request?: string, cause?: unknown);
|
|
40
|
+
}
|
|
41
|
+
/** 402 `plan_allowance_used` — the plan's included usage of the class is used, and nothing else may pay. */
|
|
42
|
+
export declare class PlanAllowanceUsedError extends UsageLimitError {
|
|
43
|
+
name: string;
|
|
44
|
+
}
|
|
45
|
+
/** 402 `paid_plan_required` — the model needs a paid plan or prepaid balance. */
|
|
46
|
+
export declare class PaidPlanRequiredError extends UsageLimitError {
|
|
47
|
+
name: string;
|
|
48
|
+
}
|
|
49
|
+
/** 429 `free_plan_cap` — the free plan's daily cap on the model is used; it lifts at `resetsAt`. */
|
|
50
|
+
export declare class FreePlanCapError extends UsageLimitError {
|
|
51
|
+
name: string;
|
|
52
|
+
}
|
|
53
|
+
/** 402 `model_cap` — the model has used its share of the plan; `fallback` answers instead. */
|
|
54
|
+
export declare class ModelCapError extends UsageLimitError {
|
|
55
|
+
name: string;
|
|
56
|
+
}
|
|
57
|
+
/** 429 `usage_cap_exceeded` — a session or day request window is spent. */
|
|
58
|
+
export declare class UsageCapExceededError extends UsageLimitError {
|
|
59
|
+
name: string;
|
|
60
|
+
}
|
|
61
|
+
/** 402 `insufficient_balance` — the known wallet balance cannot cover the request. */
|
|
62
|
+
export declare class InsufficientBalanceError extends UsageLimitError {
|
|
63
|
+
name: string;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* The typed error a refusal names, or undefined: only a 402 or 429 whose
|
|
67
|
+
* `error.code` is one of the six is one. Anything else stays whatever error its
|
|
68
|
+
* transport already raises.
|
|
69
|
+
*/
|
|
70
|
+
export declare function limited(status: number, body: unknown, request?: string, cause?: unknown): UsageLimitError | undefined;
|
|
71
|
+
/** What the X-Hanzo-* headers on one answer say. Each is undefined where the header is absent. */
|
|
72
|
+
export interface Usage {
|
|
73
|
+
/** X-Hanzo-Usage: ok | near | limited — where the class stands. */
|
|
74
|
+
usage: string | undefined;
|
|
75
|
+
/** X-Hanzo-Usage-Class: premium | ours | free. */
|
|
76
|
+
usageClass: string | undefined;
|
|
77
|
+
/** X-Hanzo-Paid-By: plan | credits | free. Prepaid and granted credit both read `credits`. */
|
|
78
|
+
paidBy: string | undefined;
|
|
79
|
+
/** X-Hanzo-Fallback: the model that answered instead (limited mode). */
|
|
80
|
+
fallback: string | undefined;
|
|
81
|
+
/** X-Hanzo-Served: the Hanzo SKU that answered. */
|
|
82
|
+
served: string | undefined;
|
|
83
|
+
/** X-Hanzo-Usage-Reason: the refusal code that caused a fallback. */
|
|
84
|
+
reason: string | undefined;
|
|
85
|
+
}
|
|
86
|
+
/** A fetch `Headers`, axios's `AxiosHeaders`, or any record of header name to value. */
|
|
87
|
+
export type HeaderSource = {
|
|
88
|
+
get(name: string): unknown;
|
|
89
|
+
} | Record<string, unknown>;
|
|
90
|
+
/**
|
|
91
|
+
* Read the usage headers off one answer: `readUsage(res.headers)` for an axios
|
|
92
|
+
* response or a fetch Response alike. Names match case-insensitively. A header
|
|
93
|
+
* that is absent, or present and empty, reads undefined — never 0 or "", which
|
|
94
|
+
* would claim the server said something it did not.
|
|
95
|
+
*/
|
|
96
|
+
export declare function readUsage(headers: HeaderSource | null | undefined): Usage;
|
|
97
|
+
/** One header by lower-case name, matched case-insensitively; undefined where absent or empty. */
|
|
98
|
+
export declare function header(headers: HeaderSource | null | undefined, name: string): string | undefined;
|
package/dist/usage.js
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// usage — how a priced request was paid for, and the refusals when it cannot be.
|
|
3
|
+
//
|
|
4
|
+
// Every AI answer says where the caller's plan stands in six headers, never a
|
|
5
|
+
// figure: X-Hanzo-Usage (ok | near | limited), -Usage-Class (premium | ours |
|
|
6
|
+
// free), -Paid-By (plan | credits | free), -Fallback (the model that answered
|
|
7
|
+
// instead), -Served (the Hanzo SKU that answered) and -Usage-Reason (the refusal
|
|
8
|
+
// code that caused a fallback). `readUsage` reads them off whatever headers the
|
|
9
|
+
// caller holds.
|
|
10
|
+
//
|
|
11
|
+
// A request nothing may pay for is refused in the OpenAI envelope,
|
|
12
|
+
//
|
|
13
|
+
// {"error": {"message", "type", "code", "class"?, "model"?, "fallback"?,
|
|
14
|
+
// "resets_at"?, "upgrade_url"?, "actions"?}}
|
|
15
|
+
//
|
|
16
|
+
// and six codes in it are six different facts with six different ways out, so
|
|
17
|
+
// each is a class: a caller tells them apart with `instanceof`, never by parsing
|
|
18
|
+
// the sentence. The names are the Python and Go SDKs' names. Both transports
|
|
19
|
+
// throw them from the error path they already had — an axios response
|
|
20
|
+
// interceptor for the generated client (transport.ts) and the six capabilities'
|
|
21
|
+
// Fault (answer.ts) — through `limited`, the one place a body becomes one.
|
|
22
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
|
+
exports.InsufficientBalanceError = exports.UsageCapExceededError = exports.ModelCapError = exports.FreePlanCapError = exports.PaidPlanRequiredError = exports.PlanAllowanceUsedError = exports.UsageLimitError = void 0;
|
|
24
|
+
exports.limited = limited;
|
|
25
|
+
exports.readUsage = readUsage;
|
|
26
|
+
exports.header = header;
|
|
27
|
+
const read_1 = require("./read");
|
|
28
|
+
/** A string the server sent, or undefined where it sent none — never "". */
|
|
29
|
+
const text = (v) => (typeof v === 'string' && v !== '' ? v : undefined);
|
|
30
|
+
/** An RFC 3339 instant, or undefined where none was sent or it does not parse. */
|
|
31
|
+
const instant = (v) => {
|
|
32
|
+
const s = text(v);
|
|
33
|
+
if (!s)
|
|
34
|
+
return undefined;
|
|
35
|
+
const d = new Date(s);
|
|
36
|
+
return Number.isNaN(d.getTime()) ? undefined : d;
|
|
37
|
+
};
|
|
38
|
+
/** A priced request refused because nothing the caller holds may pay for it. */
|
|
39
|
+
class UsageLimitError extends Error {
|
|
40
|
+
constructor(status, error, request, cause) {
|
|
41
|
+
const code = text(error['code']) ?? '';
|
|
42
|
+
super(text(error['message']) ?? `HTTP ${status} ${code}`);
|
|
43
|
+
// Literal, not `new.target.name`, which a minifier renames.
|
|
44
|
+
this.name = 'UsageLimitError';
|
|
45
|
+
this.status = status;
|
|
46
|
+
this.code = code;
|
|
47
|
+
this.type = text(error['type']);
|
|
48
|
+
this.usageClass = text(error['class']);
|
|
49
|
+
this.model = text(error['model']);
|
|
50
|
+
this.fallback = text(error['fallback']);
|
|
51
|
+
this.resetsAt = instant(error['resets_at']);
|
|
52
|
+
this.upgradeUrl = text(error['upgrade_url']);
|
|
53
|
+
// An entry with no kind is not an action, so it is not offered.
|
|
54
|
+
this.actions = (0, read_1.list)(error['actions']).flatMap((a) => {
|
|
55
|
+
const o = (0, read_1.obj)(a);
|
|
56
|
+
const kind = text(o['kind']);
|
|
57
|
+
if (!kind)
|
|
58
|
+
return [];
|
|
59
|
+
const out = { kind };
|
|
60
|
+
for (const k of ['label', 'url', 'plan', 'model']) {
|
|
61
|
+
const v = text(o[k]);
|
|
62
|
+
if (v !== undefined)
|
|
63
|
+
out[k] = v;
|
|
64
|
+
}
|
|
65
|
+
return [out];
|
|
66
|
+
});
|
|
67
|
+
this.request = request;
|
|
68
|
+
this.cause = cause;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
exports.UsageLimitError = UsageLimitError;
|
|
72
|
+
/** 402 `plan_allowance_used` — the plan's included usage of the class is used, and nothing else may pay. */
|
|
73
|
+
class PlanAllowanceUsedError extends UsageLimitError {
|
|
74
|
+
constructor() {
|
|
75
|
+
super(...arguments);
|
|
76
|
+
this.name = 'PlanAllowanceUsedError';
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
exports.PlanAllowanceUsedError = PlanAllowanceUsedError;
|
|
80
|
+
/** 402 `paid_plan_required` — the model needs a paid plan or prepaid balance. */
|
|
81
|
+
class PaidPlanRequiredError extends UsageLimitError {
|
|
82
|
+
constructor() {
|
|
83
|
+
super(...arguments);
|
|
84
|
+
this.name = 'PaidPlanRequiredError';
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
exports.PaidPlanRequiredError = PaidPlanRequiredError;
|
|
88
|
+
/** 429 `free_plan_cap` — the free plan's daily cap on the model is used; it lifts at `resetsAt`. */
|
|
89
|
+
class FreePlanCapError extends UsageLimitError {
|
|
90
|
+
constructor() {
|
|
91
|
+
super(...arguments);
|
|
92
|
+
this.name = 'FreePlanCapError';
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
exports.FreePlanCapError = FreePlanCapError;
|
|
96
|
+
/** 402 `model_cap` — the model has used its share of the plan; `fallback` answers instead. */
|
|
97
|
+
class ModelCapError extends UsageLimitError {
|
|
98
|
+
constructor() {
|
|
99
|
+
super(...arguments);
|
|
100
|
+
this.name = 'ModelCapError';
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
exports.ModelCapError = ModelCapError;
|
|
104
|
+
/** 429 `usage_cap_exceeded` — a session or day request window is spent. */
|
|
105
|
+
class UsageCapExceededError extends UsageLimitError {
|
|
106
|
+
constructor() {
|
|
107
|
+
super(...arguments);
|
|
108
|
+
this.name = 'UsageCapExceededError';
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
exports.UsageCapExceededError = UsageCapExceededError;
|
|
112
|
+
/** 402 `insufficient_balance` — the known wallet balance cannot cover the request. */
|
|
113
|
+
class InsufficientBalanceError extends UsageLimitError {
|
|
114
|
+
constructor() {
|
|
115
|
+
super(...arguments);
|
|
116
|
+
this.name = 'InsufficientBalanceError';
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
exports.InsufficientBalanceError = InsufficientBalanceError;
|
|
120
|
+
// A Map, not an object literal: a code of "constructor" must find nothing.
|
|
121
|
+
const classes = new Map([
|
|
122
|
+
['plan_allowance_used', PlanAllowanceUsedError],
|
|
123
|
+
['paid_plan_required', PaidPlanRequiredError],
|
|
124
|
+
['free_plan_cap', FreePlanCapError],
|
|
125
|
+
['model_cap', ModelCapError],
|
|
126
|
+
['usage_cap_exceeded', UsageCapExceededError],
|
|
127
|
+
['insufficient_balance', InsufficientBalanceError],
|
|
128
|
+
]);
|
|
129
|
+
/**
|
|
130
|
+
* The typed error a refusal names, or undefined: only a 402 or 429 whose
|
|
131
|
+
* `error.code` is one of the six is one. Anything else stays whatever error its
|
|
132
|
+
* transport already raises.
|
|
133
|
+
*/
|
|
134
|
+
function limited(status, body, request, cause) {
|
|
135
|
+
if (status !== 402 && status !== 429)
|
|
136
|
+
return undefined;
|
|
137
|
+
const error = (0, read_1.obj)((0, read_1.obj)(body)['error']);
|
|
138
|
+
const Class = classes.get(text(error['code']) ?? '');
|
|
139
|
+
return Class ? new Class(status, error, request, cause) : undefined;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Read the usage headers off one answer: `readUsage(res.headers)` for an axios
|
|
143
|
+
* response or a fetch Response alike. Names match case-insensitively. A header
|
|
144
|
+
* that is absent, or present and empty, reads undefined — never 0 or "", which
|
|
145
|
+
* would claim the server said something it did not.
|
|
146
|
+
*/
|
|
147
|
+
function readUsage(headers) {
|
|
148
|
+
return {
|
|
149
|
+
usage: header(headers, 'x-hanzo-usage'),
|
|
150
|
+
usageClass: header(headers, 'x-hanzo-usage-class'),
|
|
151
|
+
paidBy: header(headers, 'x-hanzo-paid-by'),
|
|
152
|
+
fallback: header(headers, 'x-hanzo-fallback'),
|
|
153
|
+
served: header(headers, 'x-hanzo-served'),
|
|
154
|
+
reason: header(headers, 'x-hanzo-usage-reason'),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
/** One header by lower-case name, matched case-insensitively; undefined where absent or empty. */
|
|
158
|
+
function header(headers, name) {
|
|
159
|
+
const h = headers ?? {};
|
|
160
|
+
let v;
|
|
161
|
+
if (typeof h.get === 'function') {
|
|
162
|
+
v = h.get(name);
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
for (const [k, value] of Object.entries(h))
|
|
166
|
+
if (k.toLowerCase() === name)
|
|
167
|
+
v = value;
|
|
168
|
+
}
|
|
169
|
+
if (Array.isArray(v))
|
|
170
|
+
v = v[0];
|
|
171
|
+
if (typeof v === 'number' || typeof v === 'boolean')
|
|
172
|
+
v = String(v);
|
|
173
|
+
return typeof v === 'string' ? text(v.trim()) : undefined;
|
|
174
|
+
}
|
package/package.json
CHANGED
package/src/answer.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
// read, so there is no arm for them.
|
|
11
11
|
|
|
12
12
|
import { list, num, obj, str } from './read';
|
|
13
|
+
import { limited } from './usage';
|
|
13
14
|
|
|
14
15
|
/** One way out of a refusal, in the order to offer them. */
|
|
15
16
|
export interface Cure {
|
|
@@ -143,15 +144,17 @@ const refusals = new Set(['spend_cap_exceeded', 'insufficient_balance']);
|
|
|
143
144
|
function refusal(body: unknown): { code: string; reason: string; product?: string; cures: Cure[] } {
|
|
144
145
|
const b = obj(body);
|
|
145
146
|
// RFC 9457 spells the code `code` and the sentence `detail`; the spend gate's
|
|
146
|
-
// own body spells them `error` and `message` and adds `product` and `cure[]
|
|
147
|
-
//
|
|
148
|
-
//
|
|
149
|
-
//
|
|
150
|
-
//
|
|
147
|
+
// own body spells them `error` and `message` and adds `product` and `cure[]`;
|
|
148
|
+
// the AI gateway's OpenAI envelope nests `code` and `message` under `error`.
|
|
149
|
+
// The spend gate's `reason` — "unpaid", "unresolved" — names the admit leg that
|
|
150
|
+
// failed rather than explaining anything to a person, so it is not read. One
|
|
151
|
+
// reader, named fallbacks — not three paths — so it keeps working unchanged
|
|
152
|
+
// when cloud settles on one envelope.
|
|
153
|
+
const e = obj(b['error']);
|
|
151
154
|
const product = str(b['product']);
|
|
152
155
|
return {
|
|
153
|
-
code: str(b['code']) || str(b['error']),
|
|
154
|
-
reason: str(b['detail']) || str(b['message']),
|
|
156
|
+
code: str(b['code']) || str(b['error']) || str(e['code']),
|
|
157
|
+
reason: str(b['detail']) || str(b['message']) || str(e['message']),
|
|
155
158
|
...(product ? { product } : {}),
|
|
156
159
|
cures: list(b['cure']).map((c) => {
|
|
157
160
|
const cure = obj(c);
|
|
@@ -193,14 +196,24 @@ export function arm<T>(r: Reply, read: (body: unknown) => T): Answer<T> {
|
|
|
193
196
|
if (r.status === 402 || (r.status === 403 && refusals.has(said.code))) {
|
|
194
197
|
return { status: 'denied', ...said, request: r.request };
|
|
195
198
|
}
|
|
196
|
-
throw
|
|
199
|
+
throw fault(r, said);
|
|
197
200
|
}
|
|
198
201
|
|
|
199
202
|
/** A read no gate refuses: the value, or a [Fault] because nothing was decided. */
|
|
200
203
|
export function value<T>(r: Reply, read: (body: unknown) => T): T {
|
|
201
204
|
if (r.status >= 200 && r.status < 300) return read(r.body);
|
|
202
|
-
|
|
203
|
-
|
|
205
|
+
throw fault(r, refusal(r.body));
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* What a call that decided nothing throws: the usage refusal the body names
|
|
210
|
+
* (usage.ts), or a [Fault]. What [arm] reads as [Denied] never reaches here.
|
|
211
|
+
*/
|
|
212
|
+
function fault(r: Reply, said: { code: string; reason: string }): Error {
|
|
213
|
+
return (
|
|
214
|
+
limited(r.status, r.body, r.request || undefined) ??
|
|
215
|
+
new Fault(r.status, said.code, said.reason || `HTTP ${r.status}`, r.request)
|
|
216
|
+
);
|
|
204
217
|
}
|
|
205
218
|
|
|
206
219
|
/** `{data, total}` — the page shape every listing on this wire answers with. */
|