@oh-my-pi/pi-ai 16.0.6 → 16.0.7
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/CHANGELOG.md +11 -0
- package/dist/types/auth-storage.d.ts +13 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/providers/anthropic.d.ts +5 -2
- package/dist/types/usage/opencode-go.d.ts +2 -0
- package/dist/types/usage.d.ts +23 -0
- package/package.json +3 -3
- package/src/auth-storage.ts +144 -5
- package/src/index.ts +1 -0
- package/src/providers/anthropic.ts +30 -9
- package/src/registry/oauth/google-oauth-shared.ts +5 -1
- package/src/registry/oauth/kimi.ts +9 -4
- package/src/usage/opencode-go.ts +89 -0
- package/src/usage.ts +24 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [16.0.7] - 2026-06-18
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- Switched Google OAuth callback hostname from `localhost` to `127.0.0.1` to prevent IPv6 loopback fallback delays and proxy routing interception.
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- Fixed OpenCode Go usage reporting to synthesize `/usage` limits from OMP-observed request costs for the 5h, weekly, and monthly provider caps. ([#2942](https://github.com/can1357/oh-my-pi/issues/2942))
|
|
14
|
+
- Fixed MiniMax Anthropic-compatible requests to serialize adaptive thinking without an invalid Anthropic `output_config.effort` tier ([#2928](https://github.com/can1357/oh-my-pi/issues/2928)).
|
|
15
|
+
|
|
5
16
|
## [16.0.6] - 2026-06-18
|
|
6
17
|
|
|
7
18
|
### Added
|
|
@@ -11,7 +11,7 @@ import { Database } from "bun:sqlite";
|
|
|
11
11
|
import type { ApiKeyResolver } from "./auth-retry";
|
|
12
12
|
import type { OAuthController, OAuthCredentials, OAuthProviderId } from "./registry/oauth/types";
|
|
13
13
|
import type { Provider } from "./types";
|
|
14
|
-
import type { CredentialRankingStrategy, UsageHistoryEntry, UsageHistoryQuery, UsageLogger, UsageProvider, UsageReport } from "./usage";
|
|
14
|
+
import type { CredentialRankingStrategy, UsageCostHistoryEntry, UsageCostHistoryQuery, UsageHistoryEntry, UsageHistoryQuery, UsageLogger, UsageProvider, UsageReport } from "./usage";
|
|
15
15
|
import { type CodexResetConsumeCode, type CodexResetCredit } from "./usage/openai-codex-reset";
|
|
16
16
|
export type ApiKeyCredential = {
|
|
17
17
|
type: "api_key";
|
|
@@ -233,6 +233,10 @@ export interface AuthCredentialStore {
|
|
|
233
233
|
* skipped — the broker host records into its own database instead.
|
|
234
234
|
*/
|
|
235
235
|
recordUsageSnapshots?(entries: UsageHistoryEntry[]): void;
|
|
236
|
+
/** Append observed request costs for providers without upstream usage APIs. */
|
|
237
|
+
recordUsageCosts?(entries: UsageCostHistoryEntry[]): void;
|
|
238
|
+
/** Read observed request costs, oldest first. */
|
|
239
|
+
listUsageCosts?(query?: UsageCostHistoryQuery): UsageCostHistoryEntry[];
|
|
236
240
|
/** Read recorded usage-limit snapshots, oldest first. */
|
|
237
241
|
listUsageHistory?(query?: UsageHistoryQuery): UsageHistoryEntry[];
|
|
238
242
|
/**
|
|
@@ -689,6 +693,12 @@ export declare class AuthStorage {
|
|
|
689
693
|
* store has no durable history (e.g. a broker-backed remote store).
|
|
690
694
|
*/
|
|
691
695
|
listUsageHistory(query?: UsageHistoryQuery): UsageHistoryEntry[];
|
|
696
|
+
/** Record one observed provider request cost for later local usage aggregation. */
|
|
697
|
+
recordUsageCost(provider: Provider, costUsd: number, options?: {
|
|
698
|
+
sessionId?: string;
|
|
699
|
+
recordedAt?: number;
|
|
700
|
+
baseUrl?: string;
|
|
701
|
+
}): boolean;
|
|
692
702
|
ingestUsageHeaders(provider: Provider, headers: Record<string, string>, options?: {
|
|
693
703
|
sessionId?: string;
|
|
694
704
|
baseUrl?: string;
|
|
@@ -944,6 +954,8 @@ export declare class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
944
954
|
cleanExpiredCache(): void;
|
|
945
955
|
recordUsageSnapshots(entries: UsageHistoryEntry[]): void;
|
|
946
956
|
listUsageHistory(query?: UsageHistoryQuery): UsageHistoryEntry[];
|
|
957
|
+
recordUsageCosts(entries: UsageCostHistoryEntry[]): void;
|
|
958
|
+
listUsageCosts(query?: UsageCostHistoryQuery): UsageCostHistoryEntry[];
|
|
947
959
|
/**
|
|
948
960
|
* Save OAuth credentials for a provider.
|
|
949
961
|
* Preserves unrelated identities and replaces only the matching credential.
|
package/dist/types/index.d.ts
CHANGED
|
@@ -36,6 +36,7 @@ export * from "./usage/kimi";
|
|
|
36
36
|
export * from "./usage/minimax-code";
|
|
37
37
|
export * from "./usage/openai-codex";
|
|
38
38
|
export * from "./usage/openai-codex-reset";
|
|
39
|
+
export * from "./usage/opencode-go";
|
|
39
40
|
export * from "./usage/zai";
|
|
40
41
|
export * from "./utils/anthropic-auth";
|
|
41
42
|
export * from "./utils/event-stream";
|
|
@@ -68,7 +68,8 @@ export declare function deriveClaudeDeviceId(installId: string, accountId?: stri
|
|
|
68
68
|
export declare function resolveAnthropicMetadataUserId(userId: unknown, isOAuthToken: boolean, sessionId?: string, accountId?: string): string | undefined;
|
|
69
69
|
export declare const applyClaudeToolPrefix: (name: string) => string;
|
|
70
70
|
export declare const stripClaudeToolPrefix: (name: string) => string;
|
|
71
|
-
export type
|
|
71
|
+
export type AnthropicOutputEffort = "low" | "medium" | "high" | "xhigh" | "max";
|
|
72
|
+
export type AnthropicEffort = AnthropicOutputEffort | "adaptive";
|
|
72
73
|
export type AnthropicThinkingDisplay = "summarized" | "omitted";
|
|
73
74
|
export interface AnthropicOptions extends StreamOptions {
|
|
74
75
|
/**
|
|
@@ -90,11 +91,13 @@ export interface AnthropicOptions extends StreamOptions {
|
|
|
90
91
|
requestModelId?: string;
|
|
91
92
|
/**
|
|
92
93
|
* Effort level for adaptive thinking.
|
|
93
|
-
* Controls how much
|
|
94
|
+
* Controls how much Claude allocates, or uses "adaptive" for MiniMax's
|
|
95
|
+
* binary adaptive-thinking tag:
|
|
94
96
|
* - "max": Always thinks with no constraints
|
|
95
97
|
* - "high": Always thinks, deep reasoning (default)
|
|
96
98
|
* - "medium": Moderate thinking, may skip for simple queries
|
|
97
99
|
* - "low": Minimal thinking, skips for simple tasks
|
|
100
|
+
* - "adaptive": Sends `thinking.type: "adaptive"` without `output_config.effort`
|
|
98
101
|
* Ignored for older models.
|
|
99
102
|
*/
|
|
100
103
|
effort?: AnthropicEffort;
|
package/dist/types/usage.d.ts
CHANGED
|
@@ -108,6 +108,23 @@ export interface UsageHistoryQuery {
|
|
|
108
108
|
/** Inclusive lower bound on {@link UsageHistoryEntry.recordedAt} (epoch ms). */
|
|
109
109
|
sinceMs?: number;
|
|
110
110
|
}
|
|
111
|
+
/** One observed provider request cost, attributed to the credential that made it. */
|
|
112
|
+
export interface UsageCostHistoryEntry {
|
|
113
|
+
/** Epoch ms the request completed. */
|
|
114
|
+
recordedAt: number;
|
|
115
|
+
provider: Provider;
|
|
116
|
+
/** Stable credential identity key (account/email/project/secret derived). */
|
|
117
|
+
accountKey: string;
|
|
118
|
+
/** Estimated request cost in USD. */
|
|
119
|
+
costUsd: number;
|
|
120
|
+
}
|
|
121
|
+
/** Filter for reading observed request costs. */
|
|
122
|
+
export interface UsageCostHistoryQuery {
|
|
123
|
+
provider?: string;
|
|
124
|
+
accountKey?: string;
|
|
125
|
+
/** Inclusive lower bound on {@link UsageCostHistoryEntry.recordedAt} (epoch ms). */
|
|
126
|
+
sinceMs?: number;
|
|
127
|
+
}
|
|
111
128
|
export declare const usageUnitSchema: import("arktype/internal/variants/string.ts").StringType<"bytes" | "minutes" | "percent" | "requests" | "tokens" | "unknown" | "usd", {}>;
|
|
112
129
|
export declare const usageStatusSchema: import("arktype/internal/variants/string.ts").StringType<"exhausted" | "ok" | "unknown" | "warning", {}>;
|
|
113
130
|
export declare const usageWindowSchema: import("arktype/internal/variants/object.ts").ObjectType<{
|
|
@@ -231,6 +248,8 @@ export interface UsageCredential {
|
|
|
231
248
|
export interface UsageFetchParams {
|
|
232
249
|
provider: Provider;
|
|
233
250
|
credential: UsageCredential;
|
|
251
|
+
/** Stable credential identity key derived by the auth storage layer. */
|
|
252
|
+
accountKey?: string;
|
|
234
253
|
baseUrl?: string;
|
|
235
254
|
signal?: AbortSignal;
|
|
236
255
|
}
|
|
@@ -239,6 +258,8 @@ export interface UsageFetchContext {
|
|
|
239
258
|
fetch: FetchImpl;
|
|
240
259
|
logger?: UsageLogger;
|
|
241
260
|
retryWait?: (delayMs: number, signal?: AbortSignal) => Promise<void>;
|
|
261
|
+
/** Observed request-cost history for providers without upstream usage APIs. */
|
|
262
|
+
listUsageCosts?: (query?: UsageCostHistoryQuery) => UsageCostHistoryEntry[];
|
|
242
263
|
}
|
|
243
264
|
/** Provider implementation for fetching usage information. */
|
|
244
265
|
export interface UsageProvider {
|
|
@@ -247,6 +268,8 @@ export interface UsageProvider {
|
|
|
247
268
|
/** Parse provider rate-limit response headers (lowercased keys) into a usage report, if supported. */
|
|
248
269
|
parseRateLimitHeaders?(headers: Record<string, string>, now?: number): UsageReport | null;
|
|
249
270
|
supports?(params: UsageFetchParams): boolean;
|
|
271
|
+
/** True when fetchUsage contacts upstream and can authenticate the credential for health checks. */
|
|
272
|
+
validatesCredentials?: boolean;
|
|
250
273
|
}
|
|
251
274
|
/** Request context used when ranking usage for a specific model. */
|
|
252
275
|
export interface CredentialRankingContext {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-ai",
|
|
4
|
-
"version": "16.0.
|
|
4
|
+
"version": "16.0.7",
|
|
5
5
|
"description": "Unified LLM API with automatic model discovery and provider configuration",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -38,8 +38,8 @@
|
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"@bufbuild/protobuf": "^2.12.0",
|
|
41
|
-
"@oh-my-pi/pi-catalog": "16.0.
|
|
42
|
-
"@oh-my-pi/pi-utils": "16.0.
|
|
41
|
+
"@oh-my-pi/pi-catalog": "16.0.7",
|
|
42
|
+
"@oh-my-pi/pi-utils": "16.0.7",
|
|
43
43
|
"arktype": "^2.2.0",
|
|
44
44
|
"partial-json": "^0.1.7",
|
|
45
45
|
"zod": "^4"
|
package/src/auth-storage.ts
CHANGED
|
@@ -21,6 +21,8 @@ import type { Provider } from "./types";
|
|
|
21
21
|
import type {
|
|
22
22
|
CredentialRankingContext,
|
|
23
23
|
CredentialRankingStrategy,
|
|
24
|
+
UsageCostHistoryEntry,
|
|
25
|
+
UsageCostHistoryQuery,
|
|
24
26
|
UsageCredential,
|
|
25
27
|
UsageFetchContext,
|
|
26
28
|
UsageFetchParams,
|
|
@@ -44,6 +46,7 @@ import {
|
|
|
44
46
|
consumeCodexResetCredit,
|
|
45
47
|
listCodexResetCredits,
|
|
46
48
|
} from "./usage/openai-codex-reset";
|
|
49
|
+
import { opencodeGoUsageProvider } from "./usage/opencode-go";
|
|
47
50
|
import { zaiUsageProvider } from "./usage/zai";
|
|
48
51
|
|
|
49
52
|
const USAGE_RANKING_METRIC_EPSILON = 1e-9;
|
|
@@ -300,6 +303,10 @@ export interface AuthCredentialStore {
|
|
|
300
303
|
* skipped — the broker host records into its own database instead.
|
|
301
304
|
*/
|
|
302
305
|
recordUsageSnapshots?(entries: UsageHistoryEntry[]): void;
|
|
306
|
+
/** Append observed request costs for providers without upstream usage APIs. */
|
|
307
|
+
recordUsageCosts?(entries: UsageCostHistoryEntry[]): void;
|
|
308
|
+
/** Read observed request costs, oldest first. */
|
|
309
|
+
listUsageCosts?(query?: UsageCostHistoryQuery): UsageCostHistoryEntry[];
|
|
303
310
|
/** Read recorded usage-limit snapshots, oldest first. */
|
|
304
311
|
listUsageHistory?(query?: UsageHistoryQuery): UsageHistoryEntry[];
|
|
305
312
|
/**
|
|
@@ -491,6 +498,7 @@ const DEFAULT_USAGE_PROVIDERS: UsageProvider[] = [
|
|
|
491
498
|
googleGeminiCliUsageProvider,
|
|
492
499
|
claudeUsageProvider,
|
|
493
500
|
zaiUsageProvider,
|
|
501
|
+
opencodeGoUsageProvider,
|
|
494
502
|
githubCopilotUsageProvider,
|
|
495
503
|
];
|
|
496
504
|
|
|
@@ -1986,7 +1994,11 @@ export class AuthStorage {
|
|
|
1986
1994
|
typeof timeoutMs === "number" && Number.isFinite(timeoutMs) && timeoutMs > 0
|
|
1987
1995
|
? AbortSignal.timeout(timeoutMs)
|
|
1988
1996
|
: undefined;
|
|
1989
|
-
let params:
|
|
1997
|
+
let params: UsageFetchParams = {
|
|
1998
|
+
...request,
|
|
1999
|
+
accountKey: this.#buildUsageCacheIdentity(request.credential),
|
|
2000
|
+
signal: timeoutSignal,
|
|
2001
|
+
};
|
|
1990
2002
|
|
|
1991
2003
|
if (
|
|
1992
2004
|
request.credential.type === "oauth" &&
|
|
@@ -2009,8 +2021,10 @@ export class AuthStorage {
|
|
|
2009
2021
|
const refreshedCredential = this.#mergeRefreshedUsageCredential(request.credential, refreshed);
|
|
2010
2022
|
this.#persistRefreshedUsageCredential(request.provider, request.credential, refreshedCredential);
|
|
2011
2023
|
params = {
|
|
2012
|
-
...
|
|
2024
|
+
...request,
|
|
2013
2025
|
credential: refreshedCredential,
|
|
2026
|
+
accountKey: this.#buildUsageCacheIdentity(refreshedCredential),
|
|
2027
|
+
signal: timeoutSignal,
|
|
2014
2028
|
};
|
|
2015
2029
|
} catch (error) {
|
|
2016
2030
|
const errorMsg = String(error);
|
|
@@ -2068,6 +2082,7 @@ export class AuthStorage {
|
|
|
2068
2082
|
return await providerImpl.fetchUsage(params, {
|
|
2069
2083
|
fetch: this.#usageFetch,
|
|
2070
2084
|
logger: this.#usageLogger,
|
|
2085
|
+
listUsageCosts: query => this.#store.listUsageCosts?.(query) ?? [],
|
|
2071
2086
|
});
|
|
2072
2087
|
} catch (error) {
|
|
2073
2088
|
logger.debug("AuthStorage usage fetch failed", {
|
|
@@ -2166,6 +2181,64 @@ export class AuthStorage {
|
|
|
2166
2181
|
return this.#store.listUsageHistory?.(query) ?? [];
|
|
2167
2182
|
}
|
|
2168
2183
|
|
|
2184
|
+
/** Record one observed provider request cost for later local usage aggregation. */
|
|
2185
|
+
recordUsageCost(
|
|
2186
|
+
provider: Provider,
|
|
2187
|
+
costUsd: number,
|
|
2188
|
+
options?: { sessionId?: string; recordedAt?: number; baseUrl?: string },
|
|
2189
|
+
): boolean {
|
|
2190
|
+
if (!Number.isFinite(costUsd) || costUsd <= 0) return false;
|
|
2191
|
+
const record = this.#store.recordUsageCosts;
|
|
2192
|
+
if (!record) return false;
|
|
2193
|
+
const credential = this.#resolveObservedUsageCredential(provider, options?.sessionId);
|
|
2194
|
+
if (!credential) return false;
|
|
2195
|
+
const entry: UsageCostHistoryEntry = {
|
|
2196
|
+
recordedAt: options?.recordedAt ?? Date.now(),
|
|
2197
|
+
provider,
|
|
2198
|
+
accountKey: this.#buildUsageCacheIdentity(credential),
|
|
2199
|
+
costUsd,
|
|
2200
|
+
};
|
|
2201
|
+
try {
|
|
2202
|
+
record.call(this.#store, [entry]);
|
|
2203
|
+
const cacheKey = this.#buildUsageReportCacheKey({
|
|
2204
|
+
provider,
|
|
2205
|
+
credential,
|
|
2206
|
+
baseUrl: options?.baseUrl,
|
|
2207
|
+
});
|
|
2208
|
+
const existing = this.#usageCache.getStale<UsageReport | null>(cacheKey);
|
|
2209
|
+
this.#usageCache.set(cacheKey, { value: existing?.value ?? null, expiresAt: Date.now() - 1 });
|
|
2210
|
+
return true;
|
|
2211
|
+
} catch (error) {
|
|
2212
|
+
this.#usageLogger?.debug("usage cost record failed", {
|
|
2213
|
+
provider,
|
|
2214
|
+
error: String(error),
|
|
2215
|
+
});
|
|
2216
|
+
return false;
|
|
2217
|
+
}
|
|
2218
|
+
}
|
|
2219
|
+
|
|
2220
|
+
#resolveObservedUsageCredential(provider: Provider, sessionId?: string): UsageCredential | undefined {
|
|
2221
|
+
const entries = this.#getStoredCredentials(provider);
|
|
2222
|
+
const sessionCredential = this.#getSessionCredential(provider, sessionId);
|
|
2223
|
+
if (sessionCredential) {
|
|
2224
|
+
const credential = entries[sessionCredential.index]?.credential;
|
|
2225
|
+
if (credential) {
|
|
2226
|
+
return credential.type === "api_key"
|
|
2227
|
+
? { type: "api_key", apiKey: credential.key }
|
|
2228
|
+
: this.#buildUsageCredential(credential);
|
|
2229
|
+
}
|
|
2230
|
+
}
|
|
2231
|
+
if (entries.length === 1) {
|
|
2232
|
+
const credential = entries[0]!.credential;
|
|
2233
|
+
return credential.type === "api_key"
|
|
2234
|
+
? { type: "api_key", apiKey: credential.key }
|
|
2235
|
+
: this.#buildUsageCredential(credential);
|
|
2236
|
+
}
|
|
2237
|
+
const envKey = getEnvApiKey(provider);
|
|
2238
|
+
if (envKey) return { type: "api_key", apiKey: envKey };
|
|
2239
|
+
return undefined;
|
|
2240
|
+
}
|
|
2241
|
+
|
|
2169
2242
|
ingestUsageHeaders(
|
|
2170
2243
|
provider: Provider,
|
|
2171
2244
|
headers: Record<string, string>,
|
|
@@ -2574,7 +2647,11 @@ export class AuthStorage {
|
|
|
2574
2647
|
const timeoutMs = options?.timeoutMs ?? this.#usageRequestTimeoutMs;
|
|
2575
2648
|
const completionProbe = options?.completionProbe;
|
|
2576
2649
|
const completionTimeoutMs = options?.completionTimeoutMs ?? timeoutMs;
|
|
2577
|
-
const ctx: UsageFetchContext = {
|
|
2650
|
+
const ctx: UsageFetchContext = {
|
|
2651
|
+
fetch: this.#usageFetch,
|
|
2652
|
+
logger: this.#usageLogger,
|
|
2653
|
+
listUsageCosts: query => this.#store.listUsageCosts?.(query) ?? [],
|
|
2654
|
+
};
|
|
2578
2655
|
|
|
2579
2656
|
const results: CredentialHealthResult[] = [];
|
|
2580
2657
|
for (const row of stored) {
|
|
@@ -2600,7 +2677,11 @@ export class AuthStorage {
|
|
|
2600
2677
|
|
|
2601
2678
|
const timeoutSignal = AbortSignal.timeout(timeoutMs);
|
|
2602
2679
|
const probeSignal = options?.signal ? AbortSignal.any([options.signal, timeoutSignal]) : timeoutSignal;
|
|
2603
|
-
let params: UsageFetchParams & { signal: AbortSignal } = {
|
|
2680
|
+
let params: UsageFetchParams & { signal: AbortSignal } = {
|
|
2681
|
+
...initialRequest,
|
|
2682
|
+
accountKey: this.#buildUsageCacheIdentity(initialRequest.credential),
|
|
2683
|
+
signal: probeSignal,
|
|
2684
|
+
};
|
|
2604
2685
|
let refreshError: string | undefined;
|
|
2605
2686
|
|
|
2606
2687
|
// Refresh expired OAuth before probing — without this an expired access
|
|
@@ -2630,7 +2711,11 @@ export class AuthStorage {
|
|
|
2630
2711
|
initialRequest.credential,
|
|
2631
2712
|
refreshedCredential,
|
|
2632
2713
|
);
|
|
2633
|
-
params = {
|
|
2714
|
+
params = {
|
|
2715
|
+
...params,
|
|
2716
|
+
credential: refreshedCredential,
|
|
2717
|
+
accountKey: this.#buildUsageCacheIdentity(refreshedCredential),
|
|
2718
|
+
};
|
|
2634
2719
|
} catch (error) {
|
|
2635
2720
|
refreshError = `oauth refresh failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
2636
2721
|
}
|
|
@@ -2651,6 +2736,8 @@ export class AuthStorage {
|
|
|
2651
2736
|
base.reason = `no usage probe configured for provider ${row.provider}`;
|
|
2652
2737
|
} else if (providerImpl.supports && !providerImpl.supports(initialRequest)) {
|
|
2653
2738
|
base.reason = `usage probe does not support ${cred.type} credentials for ${row.provider}`;
|
|
2739
|
+
} else if (providerImpl.validatesCredentials === false) {
|
|
2740
|
+
base.reason = `usage probe for ${row.provider} does not validate credentials`;
|
|
2654
2741
|
} else {
|
|
2655
2742
|
try {
|
|
2656
2743
|
const report = await providerImpl.fetchUsage(params, ctx);
|
|
@@ -4409,6 +4496,8 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
4409
4496
|
#upsertCacheStmt: Statement;
|
|
4410
4497
|
#deleteExpiredCacheStmt: Statement;
|
|
4411
4498
|
#insertUsageHistoryStmt: Statement;
|
|
4499
|
+
#insertUsageCostStmt: Statement;
|
|
4500
|
+
#listUsageCostsStmt: Statement;
|
|
4412
4501
|
#lastUsageHistoryStmt: Statement;
|
|
4413
4502
|
#listUsageHistoryStmt: Statement;
|
|
4414
4503
|
#updateUsageHistoryStmt: Statement;
|
|
@@ -4463,6 +4552,12 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
4463
4552
|
this.#listUsageHistoryStmt = this.#db.prepare(
|
|
4464
4553
|
"SELECT recorded_at, provider, account_key, email, account_id, limit_id, label, window_label, used_fraction, status, resets_at FROM usage_history WHERE recorded_at >= ? AND (? IS NULL OR provider = ?) ORDER BY recorded_at ASC",
|
|
4465
4554
|
);
|
|
4555
|
+
this.#insertUsageCostStmt = this.#db.prepare(
|
|
4556
|
+
"INSERT INTO usage_cost_history (recorded_at, provider, account_key, cost_usd) VALUES (?, ?, ?, ?)",
|
|
4557
|
+
);
|
|
4558
|
+
this.#listUsageCostsStmt = this.#db.prepare(
|
|
4559
|
+
"SELECT recorded_at, provider, account_key, cost_usd FROM usage_cost_history WHERE recorded_at >= ? AND (? IS NULL OR provider = ?) AND (? IS NULL OR account_key = ?) ORDER BY recorded_at ASC",
|
|
4560
|
+
);
|
|
4466
4561
|
}
|
|
4467
4562
|
|
|
4468
4563
|
static async open(dbPath: string = getAgentDbPath()): Promise<SqliteAuthCredentialStore> {
|
|
@@ -4543,6 +4638,14 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
4543
4638
|
resets_at INTEGER
|
|
4544
4639
|
);
|
|
4545
4640
|
CREATE INDEX IF NOT EXISTS idx_usage_history_series ON usage_history(provider, account_key, limit_id, recorded_at);
|
|
4641
|
+
CREATE TABLE IF NOT EXISTS usage_cost_history (
|
|
4642
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
4643
|
+
recorded_at INTEGER NOT NULL,
|
|
4644
|
+
provider TEXT NOT NULL,
|
|
4645
|
+
account_key TEXT NOT NULL,
|
|
4646
|
+
cost_usd REAL NOT NULL
|
|
4647
|
+
);
|
|
4648
|
+
CREATE INDEX IF NOT EXISTS idx_usage_cost_history_lookup ON usage_cost_history(provider, account_key, recorded_at);
|
|
4546
4649
|
CREATE INDEX IF NOT EXISTS idx_usage_history_recorded ON usage_history(recorded_at);
|
|
4547
4650
|
`);
|
|
4548
4651
|
|
|
@@ -5024,6 +5127,42 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
5024
5127
|
return [];
|
|
5025
5128
|
}
|
|
5026
5129
|
}
|
|
5130
|
+
recordUsageCosts(entries: UsageCostHistoryEntry[]): void {
|
|
5131
|
+
try {
|
|
5132
|
+
for (const entry of entries) {
|
|
5133
|
+
this.#insertUsageCostStmt.run(entry.recordedAt, entry.provider, entry.accountKey, entry.costUsd);
|
|
5134
|
+
}
|
|
5135
|
+
} catch {
|
|
5136
|
+
// Cost history is best-effort; never break request persistence.
|
|
5137
|
+
}
|
|
5138
|
+
}
|
|
5139
|
+
|
|
5140
|
+
listUsageCosts(query?: UsageCostHistoryQuery): UsageCostHistoryEntry[] {
|
|
5141
|
+
try {
|
|
5142
|
+
const provider = query?.provider ?? null;
|
|
5143
|
+
const accountKey = query?.accountKey ?? null;
|
|
5144
|
+
const rows = this.#listUsageCostsStmt.all(
|
|
5145
|
+
query?.sinceMs ?? 0,
|
|
5146
|
+
provider,
|
|
5147
|
+
provider,
|
|
5148
|
+
accountKey,
|
|
5149
|
+
accountKey,
|
|
5150
|
+
) as Array<{
|
|
5151
|
+
recorded_at: number;
|
|
5152
|
+
provider: string;
|
|
5153
|
+
account_key: string;
|
|
5154
|
+
cost_usd: number;
|
|
5155
|
+
}>;
|
|
5156
|
+
return rows.map(row => ({
|
|
5157
|
+
recordedAt: row.recorded_at,
|
|
5158
|
+
provider: row.provider as Provider,
|
|
5159
|
+
accountKey: row.account_key,
|
|
5160
|
+
costUsd: row.cost_usd,
|
|
5161
|
+
}));
|
|
5162
|
+
} catch {
|
|
5163
|
+
return [];
|
|
5164
|
+
}
|
|
5165
|
+
}
|
|
5027
5166
|
|
|
5028
5167
|
// ─── Convenience methods for CLI ────────────────────────────────────────
|
|
5029
5168
|
|
package/src/index.ts
CHANGED
|
@@ -36,6 +36,7 @@ export * from "./usage/kimi";
|
|
|
36
36
|
export * from "./usage/minimax-code";
|
|
37
37
|
export * from "./usage/openai-codex";
|
|
38
38
|
export * from "./usage/openai-codex-reset";
|
|
39
|
+
export * from "./usage/opencode-go";
|
|
39
40
|
export * from "./usage/zai";
|
|
40
41
|
export * from "./utils/anthropic-auth";
|
|
41
42
|
export * from "./utils/event-stream";
|
|
@@ -1003,7 +1003,8 @@ function convertContentBlocks(
|
|
|
1003
1003
|
return blocks;
|
|
1004
1004
|
}
|
|
1005
1005
|
|
|
1006
|
-
export type
|
|
1006
|
+
export type AnthropicOutputEffort = "low" | "medium" | "high" | "xhigh" | "max";
|
|
1007
|
+
export type AnthropicEffort = AnthropicOutputEffort | "adaptive";
|
|
1007
1008
|
export type AnthropicThinkingDisplay = "summarized" | "omitted";
|
|
1008
1009
|
|
|
1009
1010
|
export interface AnthropicOptions extends StreamOptions {
|
|
@@ -1026,11 +1027,13 @@ export interface AnthropicOptions extends StreamOptions {
|
|
|
1026
1027
|
requestModelId?: string;
|
|
1027
1028
|
/**
|
|
1028
1029
|
* Effort level for adaptive thinking.
|
|
1029
|
-
* Controls how much
|
|
1030
|
+
* Controls how much Claude allocates, or uses "adaptive" for MiniMax's
|
|
1031
|
+
* binary adaptive-thinking tag:
|
|
1030
1032
|
* - "max": Always thinks with no constraints
|
|
1031
1033
|
* - "high": Always thinks, deep reasoning (default)
|
|
1032
1034
|
* - "medium": Moderate thinking, may skip for simple queries
|
|
1033
1035
|
* - "low": Minimal thinking, skips for simple tasks
|
|
1036
|
+
* - "adaptive": Sends `thinking.type: "adaptive"` without `output_config.effort`
|
|
1034
1037
|
* Ignored for older models.
|
|
1035
1038
|
*/
|
|
1036
1039
|
effort?: AnthropicEffort;
|
|
@@ -1650,13 +1653,16 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
|
|
|
1650
1653
|
// `output_config.effort` ships on thinking-on requests AND on the
|
|
1651
1654
|
// thinking-off adaptive pin (adaptive-only models get effort:"low" so
|
|
1652
1655
|
// the toggle cannot 400); the beta must accompany the field in both.
|
|
1656
|
+
// MiniMax uses `thinking.type:"adaptive"` itself as the control surface,
|
|
1657
|
+
// so the sentinel "adaptive" value intentionally sends no output_config.
|
|
1653
1658
|
const sendsAdaptiveEffortPin =
|
|
1654
1659
|
options?.thinkingEnabled === false &&
|
|
1655
1660
|
model.thinking?.mode === "anthropic-adaptive" &&
|
|
1656
|
-
!model.compat.disableAdaptiveThinking
|
|
1661
|
+
!model.compat.disableAdaptiveThinking &&
|
|
1662
|
+
!usesAdaptiveThinkingTagOnly(model);
|
|
1657
1663
|
if (
|
|
1658
1664
|
model.reasoning &&
|
|
1659
|
-
(options?.thinkingEnabled || sendsAdaptiveEffortPin) &&
|
|
1665
|
+
((options?.thinkingEnabled && options.effort !== "adaptive") || sendsAdaptiveEffortPin) &&
|
|
1660
1666
|
!extraBetas.includes(effortBeta)
|
|
1661
1667
|
) {
|
|
1662
1668
|
extraBetas.push(effortBeta);
|
|
@@ -2783,11 +2789,22 @@ function enforceCacheControlLimit(params: MessageCreateParamsStreaming, maxBreak
|
|
|
2783
2789
|
}
|
|
2784
2790
|
}
|
|
2785
2791
|
|
|
2792
|
+
function usesAdaptiveThinkingTagOnly(model: Model<"anthropic-messages">): boolean {
|
|
2793
|
+
const thinking = model.thinking;
|
|
2794
|
+
if (thinking?.mode !== "anthropic-adaptive") return false;
|
|
2795
|
+
const effortMap = thinking.effortMap;
|
|
2796
|
+
if (!effortMap) return false;
|
|
2797
|
+
for (const effort of thinking.efforts) {
|
|
2798
|
+
if (effortMap[effort] !== "adaptive") return false;
|
|
2799
|
+
}
|
|
2800
|
+
return thinking.efforts.length > 0;
|
|
2801
|
+
}
|
|
2802
|
+
|
|
2786
2803
|
function resolveAnthropicAdaptiveEffort(
|
|
2787
2804
|
model: Model<"anthropic-messages">,
|
|
2788
2805
|
options: AnthropicOptions,
|
|
2789
2806
|
): AnthropicEffort | undefined {
|
|
2790
|
-
if (options.effort) return options.effort;
|
|
2807
|
+
if (options.effort) return usesAdaptiveThinkingTagOnly(model) ? "adaptive" : options.effort;
|
|
2791
2808
|
const requestedEffort = options.reasoning;
|
|
2792
2809
|
if (!requestedEffort) return undefined;
|
|
2793
2810
|
return mapEffortToAnthropicAdaptiveEffort(model, requestedEffort);
|
|
@@ -2854,7 +2871,7 @@ function buildParams(
|
|
|
2854
2871
|
|
|
2855
2872
|
// Pre-compute thinking + output_config effort.
|
|
2856
2873
|
let thinking: MessageCreateParamsStreaming["thinking"] | undefined;
|
|
2857
|
-
let outputConfigEffort:
|
|
2874
|
+
let outputConfigEffort: AnthropicOutputEffort | undefined;
|
|
2858
2875
|
if (model.reasoning) {
|
|
2859
2876
|
if (options?.thinkingEnabled) {
|
|
2860
2877
|
const mode = model.thinking?.mode;
|
|
@@ -2872,18 +2889,22 @@ function buildParams(
|
|
|
2872
2889
|
adaptive.display = options.thinkingDisplay ?? "summarized";
|
|
2873
2890
|
}
|
|
2874
2891
|
thinking = adaptive;
|
|
2875
|
-
if (effort) outputConfigEffort = effort;
|
|
2892
|
+
if (effort && effort !== "adaptive") outputConfigEffort = effort;
|
|
2876
2893
|
} else {
|
|
2877
2894
|
thinking = {
|
|
2878
2895
|
type: "enabled",
|
|
2879
2896
|
budget_tokens: options.thinkingBudgetTokens || 1024,
|
|
2880
2897
|
display: options.thinkingDisplay ?? "summarized",
|
|
2881
2898
|
};
|
|
2882
|
-
if (mode === "anthropic-budget-effort" && effort) outputConfigEffort = effort;
|
|
2899
|
+
if (mode === "anthropic-budget-effort" && effort && effort !== "adaptive") outputConfigEffort = effort;
|
|
2883
2900
|
}
|
|
2884
2901
|
} else if (options?.thinkingEnabled === false) {
|
|
2885
2902
|
const compat = model.compat;
|
|
2886
|
-
if (
|
|
2903
|
+
if (
|
|
2904
|
+
model.thinking?.mode === "anthropic-adaptive" &&
|
|
2905
|
+
!compat.disableAdaptiveThinking &&
|
|
2906
|
+
!usesAdaptiveThinkingTagOnly(model)
|
|
2907
|
+
) {
|
|
2887
2908
|
// Adaptive-only Claude models (Opus 4.6+, Sonnet 4.6+, Fable/Mythos 5) reject
|
|
2888
2909
|
// `thinking.type: "disabled"` — adaptive thinking cannot be switched off.
|
|
2889
2910
|
// Omit the thinking field (the API defaults to adaptive) and pin the
|
|
@@ -39,7 +39,11 @@ export class GoogleOAuthFlow extends OAuthCallbackFlow {
|
|
|
39
39
|
private readonly config: GoogleOAuthFlowConfig;
|
|
40
40
|
|
|
41
41
|
constructor(ctrl: OAuthController, config: GoogleOAuthFlowConfig) {
|
|
42
|
-
super(ctrl,
|
|
42
|
+
super(ctrl, {
|
|
43
|
+
preferredPort: config.callbackPort,
|
|
44
|
+
callbackPath: config.callbackPath,
|
|
45
|
+
callbackHostname: "127.0.0.1",
|
|
46
|
+
});
|
|
43
47
|
this.config = config;
|
|
44
48
|
}
|
|
45
49
|
|
|
@@ -75,15 +75,20 @@ let getDeviceId = (): string => {
|
|
|
75
75
|
return deviceId;
|
|
76
76
|
};
|
|
77
77
|
|
|
78
|
+
function sanitizeHeaderValue(value: string, fallback = ""): string {
|
|
79
|
+
const sanitized = value.replace(/[^\x20-\x7E]/g, "").trim();
|
|
80
|
+
return sanitized || fallback;
|
|
81
|
+
}
|
|
82
|
+
|
|
78
83
|
export let getKimiCommonHeaders = () => {
|
|
79
84
|
const headers = Object.freeze({
|
|
80
85
|
"User-Agent": `KimiCLI/${packageJson.version}`,
|
|
81
86
|
"X-Msh-Platform": "kimi_cli",
|
|
82
87
|
"X-Msh-Version": packageJson.version,
|
|
83
|
-
"X-Msh-Device-Name": os.hostname(),
|
|
84
|
-
"X-Msh-Device-Model": getDeviceModel(),
|
|
85
|
-
"X-Msh-Os-Version": os.version(),
|
|
86
|
-
"X-Msh-Device-Id": getDeviceId(),
|
|
88
|
+
"X-Msh-Device-Name": sanitizeHeaderValue(os.hostname(), "unknown"),
|
|
89
|
+
"X-Msh-Device-Model": sanitizeHeaderValue(getDeviceModel(), "unknown"),
|
|
90
|
+
"X-Msh-Os-Version": sanitizeHeaderValue(os.version(), "unknown"),
|
|
91
|
+
"X-Msh-Device-Id": sanitizeHeaderValue(getDeviceId(), "unknown"),
|
|
87
92
|
});
|
|
88
93
|
getKimiCommonHeaders = () => headers;
|
|
89
94
|
return headers;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { UsageCostHistoryEntry, UsageLimit, UsageProvider, UsageWindow } from "../usage";
|
|
2
|
+
|
|
3
|
+
const OPENCODE_GO_PROVIDER = "opencode-go";
|
|
4
|
+
const HOUR_MS = 60 * 60 * 1000;
|
|
5
|
+
const DAY_MS = 24 * HOUR_MS;
|
|
6
|
+
const OPENCODE_GO_LIMITS = [
|
|
7
|
+
{ id: "rolling-5h", label: "5 Hour", durationMs: 5 * HOUR_MS, limitUsd: 12 },
|
|
8
|
+
{ id: "weekly", label: "Weekly", durationMs: 7 * DAY_MS, limitUsd: 30 },
|
|
9
|
+
{ id: "monthly", label: "Monthly", durationMs: 30 * DAY_MS, limitUsd: 60 },
|
|
10
|
+
] as const;
|
|
11
|
+
|
|
12
|
+
function sumWindowCosts(entries: UsageCostHistoryEntry[], sinceMs: number): { used: number; resetsAt?: number } {
|
|
13
|
+
let used = 0;
|
|
14
|
+
let firstRecordedAt: number | undefined;
|
|
15
|
+
for (const entry of entries) {
|
|
16
|
+
if (entry.recordedAt < sinceMs) continue;
|
|
17
|
+
used += entry.costUsd;
|
|
18
|
+
if (firstRecordedAt === undefined || entry.recordedAt < firstRecordedAt) {
|
|
19
|
+
firstRecordedAt = entry.recordedAt;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return { used, resetsAt: firstRecordedAt };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function resolveStatus(usedFraction: number): UsageLimit["status"] {
|
|
26
|
+
if (usedFraction >= 1) return "exhausted";
|
|
27
|
+
if (usedFraction >= 0.8) return "warning";
|
|
28
|
+
return "ok";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function buildWindowLimit(
|
|
32
|
+
limit: (typeof OPENCODE_GO_LIMITS)[number],
|
|
33
|
+
entries: UsageCostHistoryEntry[],
|
|
34
|
+
nowMs: number,
|
|
35
|
+
): UsageLimit {
|
|
36
|
+
const sinceMs = nowMs - limit.durationMs;
|
|
37
|
+
const windowCost = sumWindowCosts(entries, sinceMs);
|
|
38
|
+
const used = Number(windowCost.used.toFixed(6));
|
|
39
|
+
const usedFraction = used / limit.limitUsd;
|
|
40
|
+
const window: UsageWindow = {
|
|
41
|
+
id: limit.id,
|
|
42
|
+
label: limit.label,
|
|
43
|
+
durationMs: limit.durationMs,
|
|
44
|
+
};
|
|
45
|
+
if (windowCost.resetsAt !== undefined) {
|
|
46
|
+
window.resetsAt = windowCost.resetsAt + limit.durationMs;
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
id: limit.id,
|
|
50
|
+
label: `${limit.label} limit`,
|
|
51
|
+
scope: {
|
|
52
|
+
provider: OPENCODE_GO_PROVIDER,
|
|
53
|
+
windowId: limit.id,
|
|
54
|
+
},
|
|
55
|
+
window,
|
|
56
|
+
amount: {
|
|
57
|
+
used,
|
|
58
|
+
limit: limit.limitUsd,
|
|
59
|
+
remaining: Math.max(0, limit.limitUsd - used),
|
|
60
|
+
usedFraction,
|
|
61
|
+
remainingFraction: Math.max(0, 1 - usedFraction),
|
|
62
|
+
unit: "usd",
|
|
63
|
+
},
|
|
64
|
+
status: resolveStatus(usedFraction),
|
|
65
|
+
notes: ["OMP-observed spend only; OpenCode usage outside OMP is not included."],
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export const opencodeGoUsageProvider: UsageProvider = {
|
|
70
|
+
id: OPENCODE_GO_PROVIDER,
|
|
71
|
+
supports: params => params.provider === OPENCODE_GO_PROVIDER && params.credential.type === "api_key",
|
|
72
|
+
validatesCredentials: false,
|
|
73
|
+
async fetchUsage(params, ctx) {
|
|
74
|
+
if (params.provider !== OPENCODE_GO_PROVIDER || params.credential.type !== "api_key") return null;
|
|
75
|
+
const nowMs = Date.now();
|
|
76
|
+
const sinceMs = nowMs - OPENCODE_GO_LIMITS[OPENCODE_GO_LIMITS.length - 1]!.durationMs;
|
|
77
|
+
const entries =
|
|
78
|
+
ctx.listUsageCosts?.({ provider: OPENCODE_GO_PROVIDER, accountKey: params.accountKey, sinceMs }) ?? [];
|
|
79
|
+
return {
|
|
80
|
+
provider: OPENCODE_GO_PROVIDER,
|
|
81
|
+
fetchedAt: nowMs,
|
|
82
|
+
limits: OPENCODE_GO_LIMITS.map(limit => buildWindowLimit(limit, entries, nowMs)),
|
|
83
|
+
metadata: {
|
|
84
|
+
planType: "OpenCode Go",
|
|
85
|
+
source: "omp-observed-request-costs",
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
},
|
|
89
|
+
};
|
package/src/usage.ts
CHANGED
|
@@ -134,6 +134,24 @@ export interface UsageHistoryQuery {
|
|
|
134
134
|
/** Inclusive lower bound on {@link UsageHistoryEntry.recordedAt} (epoch ms). */
|
|
135
135
|
sinceMs?: number;
|
|
136
136
|
}
|
|
137
|
+
/** One observed provider request cost, attributed to the credential that made it. */
|
|
138
|
+
export interface UsageCostHistoryEntry {
|
|
139
|
+
/** Epoch ms the request completed. */
|
|
140
|
+
recordedAt: number;
|
|
141
|
+
provider: Provider;
|
|
142
|
+
/** Stable credential identity key (account/email/project/secret derived). */
|
|
143
|
+
accountKey: string;
|
|
144
|
+
/** Estimated request cost in USD. */
|
|
145
|
+
costUsd: number;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Filter for reading observed request costs. */
|
|
149
|
+
export interface UsageCostHistoryQuery {
|
|
150
|
+
provider?: string;
|
|
151
|
+
accountKey?: string;
|
|
152
|
+
/** Inclusive lower bound on {@link UsageCostHistoryEntry.recordedAt} (epoch ms). */
|
|
153
|
+
sinceMs?: number;
|
|
154
|
+
}
|
|
137
155
|
|
|
138
156
|
// ─── Zod schemas (wire-shape validation for the broker `/v1/usage` endpoint) ─
|
|
139
157
|
|
|
@@ -217,6 +235,8 @@ export interface UsageCredential {
|
|
|
217
235
|
export interface UsageFetchParams {
|
|
218
236
|
provider: Provider;
|
|
219
237
|
credential: UsageCredential;
|
|
238
|
+
/** Stable credential identity key derived by the auth storage layer. */
|
|
239
|
+
accountKey?: string;
|
|
220
240
|
baseUrl?: string;
|
|
221
241
|
signal?: AbortSignal;
|
|
222
242
|
}
|
|
@@ -226,6 +246,8 @@ export interface UsageFetchContext {
|
|
|
226
246
|
fetch: FetchImpl;
|
|
227
247
|
logger?: UsageLogger;
|
|
228
248
|
retryWait?: (delayMs: number, signal?: AbortSignal) => Promise<void>;
|
|
249
|
+
/** Observed request-cost history for providers without upstream usage APIs. */
|
|
250
|
+
listUsageCosts?: (query?: UsageCostHistoryQuery) => UsageCostHistoryEntry[];
|
|
229
251
|
}
|
|
230
252
|
|
|
231
253
|
/** Provider implementation for fetching usage information. */
|
|
@@ -235,6 +257,8 @@ export interface UsageProvider {
|
|
|
235
257
|
/** Parse provider rate-limit response headers (lowercased keys) into a usage report, if supported. */
|
|
236
258
|
parseRateLimitHeaders?(headers: Record<string, string>, now?: number): UsageReport | null;
|
|
237
259
|
supports?(params: UsageFetchParams): boolean;
|
|
260
|
+
/** True when fetchUsage contacts upstream and can authenticate the credential for health checks. */
|
|
261
|
+
validatesCredentials?: boolean;
|
|
238
262
|
}
|
|
239
263
|
|
|
240
264
|
/** Request context used when ranking usage for a specific model. */
|