@dan-ai-studio/dshopencodego 0.1.43 → 0.1.44
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.en.md +48 -28
- package/README.md +47 -27
- package/lib/client.js +47 -1
- package/lib/index.js +321 -123
- package/lib/types/adapter.d.ts +26 -3
- package/lib/types/catalog/go-doc.d.ts +9 -5
- package/lib/types/catalog/index.d.ts +22 -7
- package/lib/types/catalog/service.d.ts +44 -1
- package/lib/types/config.d.ts +4 -3
- package/lib/types/conversion/replay.d.ts +30 -1
- package/lib/types/models.d.ts +14 -0
- package/lib/types/usage/meter.d.ts +89 -2
- package/package.json +20 -21
- package/lib/types/adapter.js +0 -384
- package/lib/types/catalog/constants.js +0 -18
- package/lib/types/catalog/contract.js +0 -147
- package/lib/types/catalog/gateway.js +0 -59
- package/lib/types/catalog/go-doc.js +0 -192
- package/lib/types/catalog/index.js +0 -377
- package/lib/types/catalog/json-response.js +0 -73
- package/lib/types/catalog/metadata.js +0 -278
- package/lib/types/catalog/protocol.js +0 -87
- package/lib/types/catalog/reading.js +0 -73
- package/lib/types/catalog/service.js +0 -45
- package/lib/types/config.js +0 -91
- package/lib/types/conversion/context.js +0 -212
- package/lib/types/conversion/index.js +0 -7
- package/lib/types/conversion/replay.js +0 -242
- package/lib/types/conversion/stream.js +0 -221
- package/lib/types/go-limits.js +0 -91
- package/lib/types/index.js +0 -198
- package/lib/types/models.js +0 -86
- package/lib/types/remotes.js +0 -29
- package/lib/types/session-header.js +0 -49
- package/lib/types/update/check.js +0 -138
- package/lib/types/update/contract.js +0 -64
- package/lib/types/update/index.js +0 -10
- package/lib/types/update/service.js +0 -39
- package/lib/types/update/version.js +0 -16
- package/lib/types/usage/contract.js +0 -106
- package/lib/types/usage/index.js +0 -8
- package/lib/types/usage/meter.js +0 -65
- package/lib/types/usage/service.js +0 -74
- package/lib/types/usage/windows.js +0 -84
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Host half of the update Remote.
|
|
3
|
-
*
|
|
4
|
-
* One method, one question: is a newer release of this package published?
|
|
5
|
-
* The service answers from the registry through the cached checker in
|
|
6
|
-
* `./check.ts`, so the settings page learns about updates without the model
|
|
7
|
-
* path ever touching the network. Applying an update is intentionally absent
|
|
8
|
-
* here — the page drives the host's own `pluginManager` (`removeBundle` then
|
|
9
|
-
* `installBundle`), which owns the manifest lock, peer checks, build
|
|
10
|
-
* approvals, and the HMR lifecycle a self-replacing plugin would bypass.
|
|
11
|
-
*
|
|
12
|
-
* @module @dan-ai-studio/dshopencodego/update/service
|
|
13
|
-
*/
|
|
14
|
-
import { TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol';
|
|
15
|
-
import { createUpdateChecker } from './check.js';
|
|
16
|
-
/** Update Remote: the cached registry reading for the settings page. */
|
|
17
|
-
export class OpencodeGoUpdateService extends TypertRemoteService {
|
|
18
|
-
check;
|
|
19
|
-
constructor(ctx, options) {
|
|
20
|
-
super(ctx, 'opencodeGoUpdate');
|
|
21
|
-
const checker = createUpdateChecker({ currentVersion: options.currentVersion });
|
|
22
|
-
this.check = (force) => checker.check(force);
|
|
23
|
-
}
|
|
24
|
-
/**
|
|
25
|
-
* Whether a newer release is published (cached reading).
|
|
26
|
-
*
|
|
27
|
-
* The wire method takes no arguments — the gateway client rejects any call
|
|
28
|
-
* whose positional arity differs from the descriptor's `parameters`, so a
|
|
29
|
-
* `force` flag cannot ride this call. Forced revalidation is the separate
|
|
30
|
-
* `refreshUpdate` method below, mirroring the catalog's read/refresh pair.
|
|
31
|
-
*/
|
|
32
|
-
readUpdate() {
|
|
33
|
-
return this.check(false);
|
|
34
|
-
}
|
|
35
|
-
/** Revalidate against the registry, bypassing the cache. */
|
|
36
|
-
refreshUpdate() {
|
|
37
|
-
return this.check(true);
|
|
38
|
-
}
|
|
39
|
-
}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The running build's own version.
|
|
3
|
-
*
|
|
4
|
-
* `scripts/build.mjs` replaces `__DSHOPENCODEGO_VERSION__` with the package
|
|
5
|
-
* version at bundle time, so the Host knows what it is without reading its
|
|
6
|
-
* own files back from disk. Outside the bundle (tests, source runs) the
|
|
7
|
-
* constant is absent and the value reads as a development placeholder; the
|
|
8
|
-
* update check treats an unparsable current version as "no update known"
|
|
9
|
-
* rather than erroring.
|
|
10
|
-
*
|
|
11
|
-
* @module @dan-ai-studio/dshopencodego/update/version
|
|
12
|
-
*/
|
|
13
|
-
/** Version of this build, or a placeholder outside the bundle. */
|
|
14
|
-
export const PLUGIN_VERSION = typeof __DSHOPENCODEGO_VERSION__ === 'string'
|
|
15
|
-
? __DSHOPENCODEGO_VERSION__
|
|
16
|
-
: '0.0.0-dev';
|
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Wire contract for the usage Remote.
|
|
3
|
-
*
|
|
4
|
-
* Two readings are exposed, and they answer different questions: `readWindows`
|
|
5
|
-
* asks the gateway what the account has left, and `readMeter` reports what this
|
|
6
|
-
* process has actually spent since it started. Neither is a per-model quota —
|
|
7
|
-
* the endpoint does not publish one — and the client labels them accordingly.
|
|
8
|
-
*
|
|
9
|
-
* @module @dan-ai-studio/dshopencodego/usage/contract
|
|
10
|
-
*/
|
|
11
|
-
/** Validate one window on the way back from the Host. */
|
|
12
|
-
function parseWindow(value, key) {
|
|
13
|
-
if (value === null || typeof value !== 'object')
|
|
14
|
-
throw new Error(`invalid usage window "${key}"`);
|
|
15
|
-
const row = value;
|
|
16
|
-
if ((row['status'] !== 'ok' && row['status'] !== 'rate-limited')
|
|
17
|
-
|| typeof row['percent'] !== 'number' || !Number.isFinite(row['percent'])
|
|
18
|
-
|| typeof row['resetsAt'] !== 'string') {
|
|
19
|
-
throw new Error(`invalid usage window "${key}"`);
|
|
20
|
-
}
|
|
21
|
-
return { status: row['status'], percent: row['percent'], resetsAt: row['resetsAt'] };
|
|
22
|
-
}
|
|
23
|
-
/** Validate a windows reading crossing the wire. */
|
|
24
|
-
export function parseUsageWindows(value) {
|
|
25
|
-
if (value === null || typeof value !== 'object')
|
|
26
|
-
throw new Error('invalid usage reading');
|
|
27
|
-
const row = value;
|
|
28
|
-
return {
|
|
29
|
-
...typeof row['source'] === 'string' ? { source: row['source'] } : {},
|
|
30
|
-
rolling: parseWindow(row['rolling'], 'rolling'),
|
|
31
|
-
weekly: parseWindow(row['weekly'], 'weekly'),
|
|
32
|
-
monthly: parseWindow(row['monthly'], 'monthly'),
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
function parseTotals(value) {
|
|
36
|
-
if (value === null || typeof value !== 'object')
|
|
37
|
-
throw new Error('invalid meter totals');
|
|
38
|
-
const row = value;
|
|
39
|
-
const count = (key) => {
|
|
40
|
-
const entry = row[key];
|
|
41
|
-
if (typeof entry !== 'number' || !Number.isFinite(entry) || entry < 0)
|
|
42
|
-
throw new Error(`invalid meter field "${key}"`);
|
|
43
|
-
return entry;
|
|
44
|
-
};
|
|
45
|
-
return {
|
|
46
|
-
calls: count('calls'),
|
|
47
|
-
inputTokens: count('inputTokens'),
|
|
48
|
-
outputTokens: count('outputTokens'),
|
|
49
|
-
cacheReadTokens: count('cacheReadTokens'),
|
|
50
|
-
cacheWriteTokens: count('cacheWriteTokens'),
|
|
51
|
-
totalTokens: count('totalTokens'),
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
/** Validate a meter reading crossing the wire. */
|
|
55
|
-
export function parseGoMeter(value) {
|
|
56
|
-
if (value === null || typeof value !== 'object')
|
|
57
|
-
throw new Error('invalid meter reading');
|
|
58
|
-
const row = value;
|
|
59
|
-
if (typeof row['sinceMs'] !== 'number' || typeof row['atMs'] !== 'number' || !Array.isArray(row['models'])) {
|
|
60
|
-
throw new Error('invalid meter reading');
|
|
61
|
-
}
|
|
62
|
-
return {
|
|
63
|
-
sinceMs: row['sinceMs'],
|
|
64
|
-
atMs: row['atMs'],
|
|
65
|
-
totals: parseTotals(row['totals']),
|
|
66
|
-
models: row['models'].map((entry) => {
|
|
67
|
-
if (entry === null || typeof entry !== 'object')
|
|
68
|
-
throw new Error('invalid meter model entry');
|
|
69
|
-
const model = entry['model'];
|
|
70
|
-
if (typeof model !== 'string')
|
|
71
|
-
throw new Error('invalid meter model entry');
|
|
72
|
-
return { model, ...parseTotals(entry) };
|
|
73
|
-
}),
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
/** Codec shape accepted by both released and source DSH builds. */
|
|
77
|
-
const codec = (typeSymbol, parse) => ({
|
|
78
|
-
mode: 'strict',
|
|
79
|
-
typeSymbol,
|
|
80
|
-
schema: { parse },
|
|
81
|
-
create: () => ({ parse }),
|
|
82
|
-
});
|
|
83
|
-
/** Remote methods this package owns, mounted together by the plugin. */
|
|
84
|
-
export const usageRemote = {
|
|
85
|
-
package: '@dan-ai-studio/dshopencodego',
|
|
86
|
-
descriptors: [
|
|
87
|
-
{
|
|
88
|
-
id: '@dan-ai-studio/dshopencodego#opencodeGoUsage/readWindows',
|
|
89
|
-
service: 'opencodeGoUsage',
|
|
90
|
-
namespace: 'opencodeGoUsage',
|
|
91
|
-
method: 'readWindows',
|
|
92
|
-
invocation: { kind: 'direct' },
|
|
93
|
-
parameters: [],
|
|
94
|
-
result: codec('@dan-ai-studio/dshopencodego#GoUsageWindows', parseUsageWindows),
|
|
95
|
-
},
|
|
96
|
-
{
|
|
97
|
-
id: '@dan-ai-studio/dshopencodego#opencodeGoUsage/readMeter',
|
|
98
|
-
service: 'opencodeGoUsage',
|
|
99
|
-
namespace: 'opencodeGoUsage',
|
|
100
|
-
method: 'readMeter',
|
|
101
|
-
invocation: { kind: 'direct' },
|
|
102
|
-
parameters: [],
|
|
103
|
-
result: codec('@dan-ai-studio/dshopencodego#GoMeter', parseGoMeter),
|
|
104
|
-
},
|
|
105
|
-
],
|
|
106
|
-
};
|
package/lib/types/usage/index.js
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Usage readings for the `opencode-go` route.
|
|
3
|
-
* @module @dan-ai-studio/dshopencodego/usage
|
|
4
|
-
*/
|
|
5
|
-
export { UsageMeter } from './meter.js';
|
|
6
|
-
export { parseGoUsage, readUsageWindows } from './windows.js';
|
|
7
|
-
export { parseGoMeter, parseUsageWindows, usageRemote } from './contract.js';
|
|
8
|
-
export { OpencodeGoUsageService } from './service.js';
|
package/lib/types/usage/meter.js
DELETED
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The since-boot usage meter.
|
|
3
|
-
*
|
|
4
|
-
* The gateway's `/usage` endpoint reports account percentages, not tokens, so
|
|
5
|
-
* the only honest source of per-model token counts is the usage the provider
|
|
6
|
-
* itself returns on each completed call. This meter accumulates exactly that,
|
|
7
|
-
* per model and in total, for the lifetime of the process — and says so: the
|
|
8
|
-
* number is labelled "since this Harness started", never "today".
|
|
9
|
-
*
|
|
10
|
-
* A conversation's own totals are durable and are folded from the session log
|
|
11
|
-
* by the client, which is why nothing here persists.
|
|
12
|
-
*
|
|
13
|
-
* @module @dan-ai-studio/dshopencodego/usage/meter
|
|
14
|
-
*/
|
|
15
|
-
const EMPTY = {
|
|
16
|
-
calls: 0,
|
|
17
|
-
inputTokens: 0,
|
|
18
|
-
outputTokens: 0,
|
|
19
|
-
cacheReadTokens: 0,
|
|
20
|
-
cacheWriteTokens: 0,
|
|
21
|
-
totalTokens: 0,
|
|
22
|
-
};
|
|
23
|
-
function add(totals, usage) {
|
|
24
|
-
const inputTokens = totals.inputTokens + usage.inputTokens;
|
|
25
|
-
const outputTokens = totals.outputTokens + usage.outputTokens;
|
|
26
|
-
const cacheReadTokens = totals.cacheReadTokens + (usage.cacheReadTokens ?? 0);
|
|
27
|
-
const cacheWriteTokens = totals.cacheWriteTokens + (usage.cacheWriteTokens ?? 0);
|
|
28
|
-
return {
|
|
29
|
-
calls: totals.calls + 1,
|
|
30
|
-
inputTokens,
|
|
31
|
-
outputTokens,
|
|
32
|
-
cacheReadTokens,
|
|
33
|
-
cacheWriteTokens,
|
|
34
|
-
// The four counts are disjoint (uncached input, cached input, output), so
|
|
35
|
-
// their sum is the billed total. The provider's own `totalTokens` is not
|
|
36
|
-
// used: it is optional, and mixing the two would double-count a cache read.
|
|
37
|
-
totalTokens: inputTokens + outputTokens + cacheReadTokens + cacheWriteTokens,
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
/** Accumulates provider-reported usage for the lifetime of one Host process. */
|
|
41
|
-
export class UsageMeter {
|
|
42
|
-
sinceMs = Date.now();
|
|
43
|
-
totals = EMPTY;
|
|
44
|
-
byModel = new Map();
|
|
45
|
-
/**
|
|
46
|
-
* Record one completed call.
|
|
47
|
-
* @param model - the model id the call was made with.
|
|
48
|
-
* @param usage - the provider's own usage for that call.
|
|
49
|
-
*/
|
|
50
|
-
record(model, usage) {
|
|
51
|
-
this.totals = add(this.totals, usage);
|
|
52
|
-
this.byModel.set(model, add(this.byModel.get(model) ?? EMPTY, usage));
|
|
53
|
-
}
|
|
54
|
-
/** The current reading, busiest model first. */
|
|
55
|
-
snapshot(now = Date.now()) {
|
|
56
|
-
return {
|
|
57
|
-
sinceMs: this.sinceMs,
|
|
58
|
-
atMs: now,
|
|
59
|
-
totals: this.totals,
|
|
60
|
-
models: [...this.byModel.entries()]
|
|
61
|
-
.map(([model, totals]) => ({ model, ...totals }))
|
|
62
|
-
.sort((left, right) => right.totalTokens - left.totalTokens || left.model.localeCompare(right.model)),
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
}
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Host half of the usage Remote.
|
|
3
|
-
*
|
|
4
|
-
* The credential never leaves the Host: the browser asks for a reading and gets
|
|
5
|
-
* percentages, and the Host decides whether a failed read may be shown as the
|
|
6
|
-
* previous one. A read that fails while the account is unchanged keeps the last
|
|
7
|
-
* good value and says it is stale; a read for a different endpoint or key
|
|
8
|
-
* invalidates it, because the old numbers describe a different account.
|
|
9
|
-
*
|
|
10
|
-
* @module @dan-ai-studio/dshopencodego/usage/service
|
|
11
|
-
*/
|
|
12
|
-
import { randomUUID } from 'node:crypto';
|
|
13
|
-
import { RemoteError, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol';
|
|
14
|
-
import { LlmError } from '@deepseek-ai/dsh-llm';
|
|
15
|
-
import { assertBaseURL } from '../config.js';
|
|
16
|
-
import { readUsageWindows } from './windows.js';
|
|
17
|
-
/**
|
|
18
|
-
* Decide what one failed read means for the client.
|
|
19
|
-
*
|
|
20
|
-
* A missing credential is a configuration fact, not a transient one: retrying
|
|
21
|
-
* cannot help and the reading a client holds was produced by a *different*
|
|
22
|
-
* account, so it must not be shown as this one's. Any other failure describes
|
|
23
|
-
* the account the client already has a reading for, so that reading stays valid
|
|
24
|
-
* and is marked stale.
|
|
25
|
-
* @param error - the failure raised by credential resolution or the read.
|
|
26
|
-
* @param source - identity of the account the failed read was for.
|
|
27
|
-
* @returns the domain error to send across the wire.
|
|
28
|
-
*/
|
|
29
|
-
export function usageFailure(error, source) {
|
|
30
|
-
const missing = error instanceof LlmError && error.code === 'MISSING_CREDENTIAL';
|
|
31
|
-
return new RemoteError('dshopencodego/usage-unavailable', error instanceof Error ? error.message : 'OpenCode Go usage is unavailable', missing
|
|
32
|
-
? { retryable: false, retainPrevious: false }
|
|
33
|
-
: { retryable: true, retainPrevious: true, ...source === undefined ? {} : { source } }, { cause: error });
|
|
34
|
-
}
|
|
35
|
-
/** Usage Remote: account windows from the gateway, spend from this process. */
|
|
36
|
-
export class OpencodeGoUsageService extends TypertRemoteService {
|
|
37
|
-
identity;
|
|
38
|
-
options;
|
|
39
|
-
constructor(ctx, options) {
|
|
40
|
-
super(ctx, 'opencodeGoUsage');
|
|
41
|
-
this.options = options;
|
|
42
|
-
}
|
|
43
|
-
/** The account's three windows, or a domain failure the client can render. */
|
|
44
|
-
async readWindows() {
|
|
45
|
-
const baseURL = assertBaseURL(this.options.baseURL());
|
|
46
|
-
let key;
|
|
47
|
-
try {
|
|
48
|
-
key = await this.options.resolveApiKey();
|
|
49
|
-
}
|
|
50
|
-
catch (error) {
|
|
51
|
-
this.identity = undefined;
|
|
52
|
-
throw usageFailure(error, undefined);
|
|
53
|
-
}
|
|
54
|
-
if (key === undefined || key.length === 0) {
|
|
55
|
-
this.identity = undefined;
|
|
56
|
-
throw usageFailure(new LlmError('No OpenCode Go API key is configured', 'MISSING_CREDENTIAL'), undefined);
|
|
57
|
-
}
|
|
58
|
-
if (this.identity?.baseURL !== baseURL || this.identity.key !== key) {
|
|
59
|
-
// A new account or endpoint invalidates any reading the client holds.
|
|
60
|
-
this.identity = { baseURL, key, source: randomUUID() };
|
|
61
|
-
}
|
|
62
|
-
const { source } = this.identity;
|
|
63
|
-
try {
|
|
64
|
-
return await readUsageWindows({ baseURL, apiKey: key, source });
|
|
65
|
-
}
|
|
66
|
-
catch (error) {
|
|
67
|
-
throw usageFailure(error, source);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
/** What this process has spent through the route since it started. */
|
|
71
|
-
readMeter() {
|
|
72
|
-
return this.options.meter.snapshot();
|
|
73
|
-
}
|
|
74
|
-
}
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The account's usage windows, read from the gateway.
|
|
3
|
-
*
|
|
4
|
-
* `/usage` reports three percentages — a rolling five-hour window, a week, and
|
|
5
|
-
* a month — each with a reset time and a rate-limited flag. It reports *no*
|
|
6
|
-
* per-model breakdown and no token counts, so this module never pretends to:
|
|
7
|
-
* per-model and per-session numbers are derived locally from real provider
|
|
8
|
-
* usage instead (see `./meter.ts` and the session fold in the client).
|
|
9
|
-
*
|
|
10
|
-
* @module @dan-ai-studio/dshopencodego/usage/windows
|
|
11
|
-
*/
|
|
12
|
-
import { attributionHeaders, LlmError } from '@deepseek-ai/dsh-llm';
|
|
13
|
-
import { readBoundedJson } from '../catalog/json-response.js';
|
|
14
|
-
/** Response cap: this document is a few hundred bytes. */
|
|
15
|
-
const USAGE_MAX_BYTES = 1024 * 1024;
|
|
16
|
-
/** Timeout for one usage read. */
|
|
17
|
-
const USAGE_TIMEOUT_MS = 10_000;
|
|
18
|
-
/**
|
|
19
|
-
* Parse a usage response body.
|
|
20
|
-
* @param value - the `usage` member of the response.
|
|
21
|
-
* @returns the three windows.
|
|
22
|
-
* @throws {Error} when a window is missing or malformed; an unavailable reading
|
|
23
|
-
* must never be displayed as a zero percentage.
|
|
24
|
-
*/
|
|
25
|
-
export function parseGoUsage(value) {
|
|
26
|
-
if (value === null || typeof value !== 'object')
|
|
27
|
-
throw new Error('invalid usage response');
|
|
28
|
-
const source = value;
|
|
29
|
-
const result = {};
|
|
30
|
-
for (const key of ['rolling', 'weekly', 'monthly']) {
|
|
31
|
-
const row = source[key];
|
|
32
|
-
if (row === null || row === undefined
|
|
33
|
-
|| (row.status !== 'ok' && row.status !== 'rate-limited')
|
|
34
|
-
|| typeof row.percent !== 'number' || !Number.isFinite(row.percent) || row.percent < 0
|
|
35
|
-
|| typeof row.resetsAt !== 'string' || !Number.isFinite(Date.parse(row.resetsAt))) {
|
|
36
|
-
throw new Error(`invalid usage response: window "${key}" is missing or malformed`);
|
|
37
|
-
}
|
|
38
|
-
result[key] = { status: row.status, percent: row.percent, resetsAt: row.resetsAt };
|
|
39
|
-
}
|
|
40
|
-
return result;
|
|
41
|
-
}
|
|
42
|
-
/**
|
|
43
|
-
* Read the account's usage windows.
|
|
44
|
-
* @param options - endpoint, credential, and identity.
|
|
45
|
-
* @returns the three windows, tagged with the identity that produced them.
|
|
46
|
-
* @throws {LlmError} `USAGE_UNAVAILABLE` when the credential is missing, the
|
|
47
|
-
* endpoint fails, or the body is not a usage document.
|
|
48
|
-
*/
|
|
49
|
-
export async function readUsageWindows(options) {
|
|
50
|
-
if (options.apiKey === undefined || options.apiKey.length === 0) {
|
|
51
|
-
throw new LlmError('dshopencodego: no credential is configured for the opencode-go route', 'USAGE_UNAVAILABLE');
|
|
52
|
-
}
|
|
53
|
-
const url = `${options.baseURL.replace(/\/+$/, '')}/usage`;
|
|
54
|
-
const timeout = AbortSignal.timeout(USAGE_TIMEOUT_MS);
|
|
55
|
-
let response;
|
|
56
|
-
try {
|
|
57
|
-
response = await fetch(url, {
|
|
58
|
-
redirect: 'error',
|
|
59
|
-
headers: {
|
|
60
|
-
...attributionHeaders(),
|
|
61
|
-
accept: 'application/json',
|
|
62
|
-
authorization: `Bearer ${options.apiKey}`,
|
|
63
|
-
},
|
|
64
|
-
signal: options.signal === undefined ? timeout : AbortSignal.any([options.signal, timeout]),
|
|
65
|
-
});
|
|
66
|
-
}
|
|
67
|
-
catch (error) {
|
|
68
|
-
throw new LlmError(`could not reach ${url}`, 'USAGE_UNAVAILABLE', { cause: error });
|
|
69
|
-
}
|
|
70
|
-
if (!response.ok) {
|
|
71
|
-
await response.body?.cancel().catch(() => { });
|
|
72
|
-
throw new LlmError(`${url} answered HTTP ${response.status}`, 'USAGE_UNAVAILABLE');
|
|
73
|
-
}
|
|
74
|
-
const body = await readBoundedJson(response, url, USAGE_MAX_BYTES);
|
|
75
|
-
try {
|
|
76
|
-
return {
|
|
77
|
-
...parseGoUsage(body?.usage),
|
|
78
|
-
...options.source === undefined ? {} : { source: options.source },
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
catch (error) {
|
|
82
|
-
throw new LlmError(`${url} returned an invalid usage document`, 'USAGE_UNAVAILABLE', { cause: error });
|
|
83
|
-
}
|
|
84
|
-
}
|