@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 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.
@@ -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 AnthropicEffort = "low" | "medium" | "high" | "xhigh" | "max";
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 thinking Claude allocates:
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;
@@ -0,0 +1,2 @@
1
+ import type { UsageProvider } from "../usage";
2
+ export declare const opencodeGoUsageProvider: UsageProvider;
@@ -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.6",
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.6",
42
- "@oh-my-pi/pi-utils": "16.0.6",
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"
@@ -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: UsageRequestDescriptor & { signal?: AbortSignal } = { ...request, signal: timeoutSignal };
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
- ...params,
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 = { fetch: this.#usageFetch, logger: this.#usageLogger };
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 } = { ...initialRequest, signal: probeSignal };
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 = { ...params, credential: refreshedCredential };
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 AnthropicEffort = "low" | "medium" | "high" | "xhigh" | "max";
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 thinking Claude allocates:
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: AnthropicEffort | undefined;
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 (model.thinking?.mode === "anthropic-adaptive" && !compat.disableAdaptiveThinking) {
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, config.callbackPort, config.callbackPath);
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. */