@oh-my-pi/pi-ai 16.0.6 → 16.0.8
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 +20 -0
- package/dist/types/auth-storage.d.ts +13 -1
- package/dist/types/index.d.ts +3 -2
- 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/dist/types/utils/validation.d.ts +2 -1
- package/package.json +3 -3
- package/src/auth-broker/snapshot-cache.ts +21 -5
- package/src/auth-storage.ts +157 -9
- package/src/index.ts +3 -2
- package/src/providers/anthropic.ts +30 -9
- package/src/providers/cursor.ts +37 -29
- 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/src/utils/validation.ts +12 -7
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,26 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [16.0.8] - 2026-06-18
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Improved reliability of auth-broker snapshot loading by implementing a robust manual schema check
|
|
10
|
+
- Fixed MCP tool argument validation to drop optional empty-string parameters before schema validation, matching the existing optional null handling and avoiding pattern/type failures for omitted model-filled fields. ([#2981](https://github.com/can1357/oh-my-pi/issues/2981))
|
|
11
|
+
- Fixed API-key credential replacement to hard-delete superseded disabled `api_key` rows so `auth_credentials` does not grow indefinitely after key rotation. ([#2941](https://github.com/can1357/oh-my-pi/issues/2941))
|
|
12
|
+
- Fixed Cursor provider streaming to close text blocks before tool calls so post-tool text opens a new content block and TUI transcript cards render inline instead of grouped near the bottom. ([#2924](https://github.com/can1357/oh-my-pi/issues/2924))
|
|
13
|
+
|
|
14
|
+
## [16.0.7] - 2026-06-18
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- Switched Google OAuth callback hostname from `localhost` to `127.0.0.1` to prevent IPv6 loopback fallback delays and proxy routing interception.
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- 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))
|
|
23
|
+
- 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)).
|
|
24
|
+
|
|
5
25
|
## [16.0.6] - 2026-06-18
|
|
6
26
|
|
|
7
27
|
### 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
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
export { type Type, type } from "arktype";
|
|
2
2
|
export { type ZodType, z } from "zod/v4";
|
|
3
3
|
export * from "./api-registry";
|
|
4
|
-
export * from "./auth-broker";
|
|
5
|
-
export {
|
|
4
|
+
export type * from "./auth-broker";
|
|
5
|
+
export type { AuthGatewayBootOptions, ModelResolver } from "./auth-gateway/server";
|
|
6
6
|
export * from "./auth-gateway/types";
|
|
7
7
|
export * from "./auth-retry";
|
|
8
8
|
export * from "./auth-storage";
|
|
@@ -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 {
|
|
@@ -10,7 +10,8 @@ export declare function validateToolCall(tools: Tool[], toolCall: ToolCall): Too
|
|
|
10
10
|
/**
|
|
11
11
|
* Validates tool call arguments against the tool's schema (Zod or plain JSON
|
|
12
12
|
* Schema). Applies LLM-quirk coercions (numeric strings, JSON-string
|
|
13
|
-
* containers, null-for-optional, null-for-default) before
|
|
13
|
+
* containers, null/invalid-empty-string-for-optional, null-for-default) before
|
|
14
|
+
* declaring failure.
|
|
14
15
|
*
|
|
15
16
|
* @throws Error with a formatted message when validation cannot be reconciled.
|
|
16
17
|
*/
|
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.8",
|
|
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.8",
|
|
42
|
+
"@oh-my-pi/pi-utils": "16.0.8",
|
|
43
43
|
"arktype": "^2.2.0",
|
|
44
44
|
"partial-json": "^0.1.7",
|
|
45
45
|
"zod": "^4"
|
|
@@ -9,9 +9,7 @@
|
|
|
9
9
|
import * as fs from "node:fs/promises";
|
|
10
10
|
import * as path from "node:path";
|
|
11
11
|
import { isEnoent, logger } from "@oh-my-pi/pi-utils";
|
|
12
|
-
import { type } from "arktype";
|
|
13
12
|
import type { SnapshotResponse } from "./types";
|
|
14
|
-
import { snapshotResponseSchema } from "./wire-schemas";
|
|
15
13
|
|
|
16
14
|
const MAGIC = new Uint8Array([0x4f, 0x4d, 0x50, 0x53]); // "OMPS"
|
|
17
15
|
const VERSION = 1;
|
|
@@ -40,6 +38,25 @@ export interface WriteAuthBrokerSnapshotCacheOptions {
|
|
|
40
38
|
snapshot: SnapshotResponse;
|
|
41
39
|
}
|
|
42
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Cheap structural guard for a decrypted cache payload. The bytes are already
|
|
43
|
+
* AES-256-GCM authenticated, so this only rejects shape/version drift (a cache
|
|
44
|
+
* written by a different omp build, or a buggy write) — not tampering. A
|
|
45
|
+
* mismatch returns null so the caller refetches a fresh snapshot.
|
|
46
|
+
*/
|
|
47
|
+
function isSnapshotResponseShape(v: unknown): v is SnapshotResponse {
|
|
48
|
+
if (typeof v !== "object" || v === null) return false;
|
|
49
|
+
const o = v as Record<string, unknown>;
|
|
50
|
+
return (
|
|
51
|
+
typeof o.generation === "number" &&
|
|
52
|
+
typeof o.generatedAt === "number" &&
|
|
53
|
+
typeof o.serverNowMs === "number" &&
|
|
54
|
+
typeof o.refresher === "object" &&
|
|
55
|
+
o.refresher !== null &&
|
|
56
|
+
Array.isArray(o.credentials)
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
43
60
|
export async function readAuthBrokerSnapshotCache(
|
|
44
61
|
opts: ReadAuthBrokerSnapshotCacheOptions,
|
|
45
62
|
): Promise<SnapshotResponse | null> {
|
|
@@ -56,12 +73,11 @@ export async function readAuthBrokerSnapshotCache(
|
|
|
56
73
|
const plaintext = await decryptCachePayload(data, opts.token, opts.url);
|
|
57
74
|
if (!plaintext) return null;
|
|
58
75
|
const parsed: unknown = JSON.parse(TEXT_DECODER.decode(plaintext));
|
|
59
|
-
|
|
60
|
-
if (result instanceof type.errors) {
|
|
76
|
+
if (!isSnapshotResponseShape(parsed)) {
|
|
61
77
|
logger.debug("auth-broker snapshot cache schema invalid", { path: opts.path });
|
|
62
78
|
return null;
|
|
63
79
|
}
|
|
64
|
-
const snapshot =
|
|
80
|
+
const snapshot = parsed;
|
|
65
81
|
const now = opts.now?.() ?? Date.now();
|
|
66
82
|
if (now - snapshot.generatedAt > opts.ttlMs) return null;
|
|
67
83
|
return snapshot;
|
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
|
|
|
@@ -4850,21 +4953,30 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
4850
4953
|
}
|
|
4851
4954
|
|
|
4852
4955
|
/**
|
|
4853
|
-
* Hard-deletes disabled rows for a provider when an active
|
|
4854
|
-
*
|
|
4855
|
-
*
|
|
4956
|
+
* Hard-deletes disabled rows for a provider when an active replacement exists.
|
|
4957
|
+
* OAuth credentials match by identity key; API keys match by provider and type.
|
|
4958
|
+
* Disabled rows without an active same-type replacement remain recoverable.
|
|
4856
4959
|
*/
|
|
4857
4960
|
#purgeSupersededDisabledRows(provider: string, activeRows: StoredAuthCredential[]): void {
|
|
4858
4961
|
try {
|
|
4962
|
+
let hasActiveApiKey = false;
|
|
4859
4963
|
const activeIdentityKeys = new Set<string>();
|
|
4860
4964
|
for (const row of activeRows) {
|
|
4965
|
+
if (row.credential.type === "api_key") {
|
|
4966
|
+
hasActiveApiKey = true;
|
|
4967
|
+
continue;
|
|
4968
|
+
}
|
|
4861
4969
|
const identityKey = resolveCredentialIdentityKey(provider, row.credential);
|
|
4862
4970
|
if (identityKey) activeIdentityKeys.add(identityKey);
|
|
4863
4971
|
}
|
|
4864
|
-
if (activeIdentityKeys.size === 0) return;
|
|
4972
|
+
if (!hasActiveApiKey && activeIdentityKeys.size === 0) return;
|
|
4865
4973
|
|
|
4866
4974
|
const disabledRows = this.#listDisabledByProviderStmt.all(provider) as AuthRow[];
|
|
4867
4975
|
for (const row of disabledRows) {
|
|
4976
|
+
if (hasActiveApiKey && row.credential_type === "api_key") {
|
|
4977
|
+
this.#hardDeleteStmt.run(row.id);
|
|
4978
|
+
continue;
|
|
4979
|
+
}
|
|
4868
4980
|
const identityKey = resolveRowCredentialIdentityKey(provider, row);
|
|
4869
4981
|
if (identityKey && activeIdentityKeys.has(identityKey)) {
|
|
4870
4982
|
this.#hardDeleteStmt.run(row.id);
|
|
@@ -5024,6 +5136,42 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
5024
5136
|
return [];
|
|
5025
5137
|
}
|
|
5026
5138
|
}
|
|
5139
|
+
recordUsageCosts(entries: UsageCostHistoryEntry[]): void {
|
|
5140
|
+
try {
|
|
5141
|
+
for (const entry of entries) {
|
|
5142
|
+
this.#insertUsageCostStmt.run(entry.recordedAt, entry.provider, entry.accountKey, entry.costUsd);
|
|
5143
|
+
}
|
|
5144
|
+
} catch {
|
|
5145
|
+
// Cost history is best-effort; never break request persistence.
|
|
5146
|
+
}
|
|
5147
|
+
}
|
|
5148
|
+
|
|
5149
|
+
listUsageCosts(query?: UsageCostHistoryQuery): UsageCostHistoryEntry[] {
|
|
5150
|
+
try {
|
|
5151
|
+
const provider = query?.provider ?? null;
|
|
5152
|
+
const accountKey = query?.accountKey ?? null;
|
|
5153
|
+
const rows = this.#listUsageCostsStmt.all(
|
|
5154
|
+
query?.sinceMs ?? 0,
|
|
5155
|
+
provider,
|
|
5156
|
+
provider,
|
|
5157
|
+
accountKey,
|
|
5158
|
+
accountKey,
|
|
5159
|
+
) as Array<{
|
|
5160
|
+
recorded_at: number;
|
|
5161
|
+
provider: string;
|
|
5162
|
+
account_key: string;
|
|
5163
|
+
cost_usd: number;
|
|
5164
|
+
}>;
|
|
5165
|
+
return rows.map(row => ({
|
|
5166
|
+
recordedAt: row.recorded_at,
|
|
5167
|
+
provider: row.provider as Provider,
|
|
5168
|
+
accountKey: row.account_key,
|
|
5169
|
+
costUsd: row.cost_usd,
|
|
5170
|
+
}));
|
|
5171
|
+
} catch {
|
|
5172
|
+
return [];
|
|
5173
|
+
}
|
|
5174
|
+
}
|
|
5027
5175
|
|
|
5028
5176
|
// ─── Convenience methods for CLI ────────────────────────────────────────
|
|
5029
5177
|
|
package/src/index.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
export { type Type, type } from "arktype";
|
|
2
2
|
export { type ZodType, z } from "zod/v4";
|
|
3
3
|
export * from "./api-registry";
|
|
4
|
-
export * from "./auth-broker";
|
|
5
|
-
export {
|
|
4
|
+
export type * from "./auth-broker";
|
|
5
|
+
export type { AuthGatewayBootOptions, ModelResolver } from "./auth-gateway/server";
|
|
6
6
|
export * from "./auth-gateway/types";
|
|
7
7
|
export * from "./auth-retry";
|
|
8
8
|
export * from "./auth-storage";
|
|
@@ -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
|
package/src/providers/cursor.ts
CHANGED
|
@@ -548,24 +548,8 @@ export const streamCursor: StreamFunction<"cursor-agent"> = (
|
|
|
548
548
|
}
|
|
549
549
|
});
|
|
550
550
|
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
stream.push({
|
|
554
|
-
type: "text_end",
|
|
555
|
-
contentIndex: idx,
|
|
556
|
-
content: state.currentTextBlock.text,
|
|
557
|
-
partial: output,
|
|
558
|
-
});
|
|
559
|
-
}
|
|
560
|
-
if (state.currentThinkingBlock) {
|
|
561
|
-
const idx = output.content.indexOf(state.currentThinkingBlock);
|
|
562
|
-
stream.push({
|
|
563
|
-
type: "thinking_end",
|
|
564
|
-
contentIndex: idx,
|
|
565
|
-
content: state.currentThinkingBlock.thinking,
|
|
566
|
-
partial: output,
|
|
567
|
-
});
|
|
568
|
-
}
|
|
551
|
+
endCurrentTextBlock(output, stream, state);
|
|
552
|
+
endCurrentThinkingBlock(output, stream, state);
|
|
569
553
|
if (state.currentToolCall) {
|
|
570
554
|
const idx = output.content.indexOf(state.currentToolCall);
|
|
571
555
|
state.currentToolCall.arguments = parseStreamingJson(state.currentToolCall.partialJson);
|
|
@@ -1972,6 +1956,38 @@ export function mergeCursorMcpToolCallArgs(
|
|
|
1972
1956
|
return merged;
|
|
1973
1957
|
}
|
|
1974
1958
|
|
|
1959
|
+
function endCurrentTextBlock(output: AssistantMessage, stream: AssistantMessageEventStream, state: BlockState): void {
|
|
1960
|
+
const block = state.currentTextBlock;
|
|
1961
|
+
if (!block) return;
|
|
1962
|
+
const idx = output.content.indexOf(block);
|
|
1963
|
+
delete (block as { index?: number }).index;
|
|
1964
|
+
stream.push({
|
|
1965
|
+
type: "text_end",
|
|
1966
|
+
contentIndex: idx,
|
|
1967
|
+
content: block.text,
|
|
1968
|
+
partial: output,
|
|
1969
|
+
});
|
|
1970
|
+
state.setTextBlock(null);
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1973
|
+
function endCurrentThinkingBlock(
|
|
1974
|
+
output: AssistantMessage,
|
|
1975
|
+
stream: AssistantMessageEventStream,
|
|
1976
|
+
state: BlockState,
|
|
1977
|
+
): void {
|
|
1978
|
+
const block = state.currentThinkingBlock;
|
|
1979
|
+
if (!block) return;
|
|
1980
|
+
const idx = output.content.indexOf(block);
|
|
1981
|
+
delete (block as { index?: number }).index;
|
|
1982
|
+
stream.push({
|
|
1983
|
+
type: "thinking_end",
|
|
1984
|
+
contentIndex: idx,
|
|
1985
|
+
content: block.thinking,
|
|
1986
|
+
partial: output,
|
|
1987
|
+
});
|
|
1988
|
+
state.setThinkingBlock(null);
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1975
1991
|
/** Exported for tests: drives one Cursor interaction update through the streaming state machine. */
|
|
1976
1992
|
export function processInteractionUpdate(
|
|
1977
1993
|
update: any,
|
|
@@ -2017,18 +2033,10 @@ export function processInteractionUpdate(
|
|
|
2017
2033
|
const idx = output.content.indexOf(state.currentThinkingBlock!);
|
|
2018
2034
|
stream.push({ type: "thinking_delta", contentIndex: idx, delta, partial: output });
|
|
2019
2035
|
} else if (updateCase === "thinkingCompleted") {
|
|
2020
|
-
|
|
2021
|
-
const idx = output.content.indexOf(state.currentThinkingBlock);
|
|
2022
|
-
delete (state.currentThinkingBlock as any).index;
|
|
2023
|
-
stream.push({
|
|
2024
|
-
type: "thinking_end",
|
|
2025
|
-
contentIndex: idx,
|
|
2026
|
-
content: state.currentThinkingBlock.thinking,
|
|
2027
|
-
partial: output,
|
|
2028
|
-
});
|
|
2029
|
-
state.setThinkingBlock(null);
|
|
2030
|
-
}
|
|
2036
|
+
endCurrentThinkingBlock(output, stream, state);
|
|
2031
2037
|
} else if (updateCase === "toolCallStarted") {
|
|
2038
|
+
endCurrentTextBlock(output, stream, state);
|
|
2039
|
+
endCurrentThinkingBlock(output, stream, state);
|
|
2032
2040
|
const toolCall = update.message.value.toolCall;
|
|
2033
2041
|
if (toolCall) {
|
|
2034
2042
|
const mcpCall = toolCall.mcpToolCall;
|
|
@@ -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. */
|
package/src/utils/validation.ts
CHANGED
|
@@ -764,10 +764,13 @@ function normalizeOptionalNullsForSchema(
|
|
|
764
764
|
if (!(key in nextValue)) continue;
|
|
765
765
|
const currentValue = nextValue[key];
|
|
766
766
|
const isNullish = currentValue === null || currentValue === "null";
|
|
767
|
+
const isInvalidEmptyString =
|
|
768
|
+
currentValue === "" && !required.has(key) && !branchMatchesSchema(propertySchema, currentValue);
|
|
767
769
|
|
|
768
|
-
// Strip null
|
|
769
|
-
//
|
|
770
|
-
|
|
770
|
+
// Strip null/string "null" from optional fields, and strip empty
|
|
771
|
+
// strings only when the property schema would reject the explicit value.
|
|
772
|
+
// LLMs sometimes output these placeholders to mean "no value".
|
|
773
|
+
if ((isNullish || isInvalidEmptyString) && !required.has(key)) {
|
|
771
774
|
if (!changed) {
|
|
772
775
|
nextValue = { ...nextValue };
|
|
773
776
|
changed = true;
|
|
@@ -1281,7 +1284,8 @@ function truncateArgsForError(value: unknown): unknown {
|
|
|
1281
1284
|
/**
|
|
1282
1285
|
* Validates tool call arguments against the tool's schema (Zod or plain JSON
|
|
1283
1286
|
* Schema). Applies LLM-quirk coercions (numeric strings, JSON-string
|
|
1284
|
-
* containers, null-for-optional, null-for-default) before
|
|
1287
|
+
* containers, null/invalid-empty-string-for-optional, null-for-default) before
|
|
1288
|
+
* declaring failure.
|
|
1285
1289
|
*
|
|
1286
1290
|
* @throws Error with a formatted message when validation cannot be reconciled.
|
|
1287
1291
|
*/
|
|
@@ -1290,9 +1294,10 @@ export function validateToolArguments(tool: Tool, toolCall: ToolCall): ToolCall[
|
|
|
1290
1294
|
const ctx = getValidationContext(tool);
|
|
1291
1295
|
const { json } = ctx;
|
|
1292
1296
|
|
|
1293
|
-
// Always normalize first — strip null
|
|
1294
|
-
//
|
|
1295
|
-
//
|
|
1297
|
+
// Always normalize first — strip null/string "null" from optional fields,
|
|
1298
|
+
// strip optional empty strings only when their property schema rejects the
|
|
1299
|
+
// explicit value, and substitute defaults. Handles LLM outputting
|
|
1300
|
+
// placeholders for "no value" even when validation would otherwise pass.
|
|
1296
1301
|
let normalizedArgs: unknown = originalArgs;
|
|
1297
1302
|
let changed = false;
|
|
1298
1303
|
const initialNormalization = normalizeOptionalNullsForSchema(json, normalizedArgs);
|