@oh-my-pi/pi-ai 18.1.11 → 18.1.12

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,10 +2,17 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [18.1.12] - 2026-09-06
6
+
7
+ ### Added
8
+
9
+ - Added Muse Code subscription sign-in, credential refresh, inference, and quota reporting in `/usage`, with durable rate-limit backoff so quota refresh recovers instead of repeatedly retrying.
10
+
5
11
  ## [18.1.11] - 2026-09-05
6
12
 
7
13
  ### Fixed
8
14
 
15
+ - Fixed OpenCode Go usage polls (`GET /zen/go/v1/usage`) missing `x-opencode-session` and omp's `User-Agent`: background polls now attribute with the stable install id so the requests OpenCode flags as `Bun fetch` carry the required session header.
9
16
  - GitHub Copilot sign-in now requests only basic profile access, restoring login for Enterprise organizations that reject repository, gist, and Codespaces permissions ([#10656](https://github.com/can1357/oh-my-pi/issues/10656)).
10
17
 
11
18
  ## [18.1.9] - 2026-09-04
@@ -22,6 +22,8 @@ export type OAuthErrorKind =
22
22
  | "configuration"
23
23
  /** Cloud project provisioning / onboarding (loadCodeAssist, onboardUser). */
24
24
  | "provisioning"
25
+ /** Subscription/payment gate: plan required or purchase action needed. */
26
+ | "entitlement"
25
27
  /** OIDC / endpoint discovery failed. */
26
28
  | "discovery";
27
29
  export interface OAuthErrorOptions {
@@ -35,6 +35,7 @@ export * from "./usage/github-copilot.js";
35
35
  export * from "./usage/google-antigravity.js";
36
36
  export * from "./usage/kimi.js";
37
37
  export * from "./usage/minimax-code.js";
38
+ export * from "./usage/muse-code.js";
38
39
  export * from "./usage/ollama.js";
39
40
  export * from "./usage/openai-codex.js";
40
41
  export * from "./usage/openai-codex-reset.js";
@@ -357,11 +357,12 @@ export declare function resolveOpenAICompletionsOutputClamp(model: Model<"openai
357
357
  /**
358
358
  * Provider-specific Responses API output clamp.
359
359
  *
360
- * Meta documents a 131,072-token output limit for Muse Spark 1.1, so native
361
- * Meta requests may use the model's full advertised cap instead of the
362
- * conservative 64k OpenAI-compatible default.
360
+ * Models whose compiled provider policy opts in may use their full advertised
361
+ * output cap instead of the conservative 64k OpenAI-compatible default.
363
362
  */
364
- export declare function resolveOpenAIResponsesOutputClamp(model: Pick<Model, "provider" | "maxTokens">): number | undefined;
363
+ export declare function resolveOpenAIResponsesOutputClamp(model: Pick<Model, "maxTokens"> & {
364
+ compat: Pick<ResolvedOpenAISharedCompat, "clampOutputToModelMax">;
365
+ }): number | undefined;
365
366
  /**
366
367
  * Enable `tool_stream` for Z.AI/GLM-5.2 reasoning models when tools are present
367
368
  * (GLM-5.2 streams tool-call arguments incrementally and needs the flag to do so).
@@ -659,7 +660,7 @@ type CommonSamplingOptions = Pick<StreamOptions, "temperature" | "topP" | "topK"
659
660
  * reflect the model's context window rather than the upstream output limit.
660
661
  */
661
662
  export declare function applyCommonResponsesSamplingParams<P extends CommonResponsesParams>(params: P, options: CommonSamplingOptions | undefined, model: Pick<Model, "provider" | "api" | "id" | "omitMaxOutputTokens" | "maxTokens" | "identity"> & {
662
- compat: Pick<ResolvedOpenAISharedCompat, "supportsSamplingParams" | "supportsPenaltyAndStopParams">;
663
+ compat: Pick<ResolvedOpenAISharedCompat, "supportsSamplingParams" | "supportsPenaltyAndStopParams" | "clampOutputToModelMax">;
663
664
  }): void;
664
665
  type ReasoningOptions = {
665
666
  reasoning?: string;
@@ -0,0 +1,3 @@
1
+ import type { ProviderTransport } from "./build.js";
2
+ /** Muse stores both the Meta account token and its subscription-minted Model API key in one OAuth bearer. */
3
+ export declare const museCodeTransport: ProviderTransport;
@@ -22,6 +22,7 @@ export declare function unregisterOAuthProviders(sourceId: string): void;
22
22
  * Refresh a built-in OAuth grant, cancelling provider work when refresh ownership ends.
23
23
  */
24
24
  export declare function refreshOAuthToken(provider: OAuthProvider, credentials: OAuthCredentials, signal?: AbortSignal): Promise<OAuthCredentials>;
25
+ export declare function normalizeOAuthCredentialExpiry<T extends OAuthCredentials>(provider: string, credentials: T): T;
25
26
  /**
26
27
  * Build API-key bytes for a provider from an already-fresh OAuth credential.
27
28
  *
@@ -0,0 +1,67 @@
1
+ import type { AfterExchangeHook } from "../hooks/types.js";
2
+ import type { FetchImpl } from "../../types.js";
3
+ declare const museCodeKeyResponseSchema: import("@oh-my-pi/omptype").FluentType<{
4
+ action_url?: string | null | undefined;
5
+ api_key?: string | undefined;
6
+ is_subs_active?: boolean | undefined;
7
+ require_payment?: boolean | undefined;
8
+ require_payment_action_url?: string | undefined;
9
+ subs_tier_id?: string | undefined;
10
+ subs_tier_name?: string | undefined;
11
+ subs_usage?: {
12
+ weekly?: {
13
+ resets_at?: string | number | undefined;
14
+ used_percent?: number | undefined;
15
+ window_duration_mins?: number | undefined;
16
+ } | null | undefined;
17
+ window?: {
18
+ resets_at?: string | number | undefined;
19
+ used_percent?: number | undefined;
20
+ window_duration_mins?: number | undefined;
21
+ } | null | undefined;
22
+ } | null | undefined;
23
+ user_email?: string | undefined;
24
+ user_id?: string | undefined;
25
+ }, {
26
+ action_url?: string | null | undefined;
27
+ api_key?: string | undefined;
28
+ is_subs_active?: boolean | undefined;
29
+ require_payment?: boolean | undefined;
30
+ require_payment_action_url?: string | undefined;
31
+ subs_tier_id?: string | undefined;
32
+ subs_tier_name?: string | undefined;
33
+ subs_usage?: {
34
+ weekly?: {
35
+ resets_at?: string | number | undefined;
36
+ used_percent?: number | undefined;
37
+ window_duration_mins?: number | undefined;
38
+ } | null | undefined;
39
+ window?: {
40
+ resets_at?: string | number | undefined;
41
+ used_percent?: number | undefined;
42
+ window_duration_mins?: number | undefined;
43
+ } | null | undefined;
44
+ } | null | undefined;
45
+ user_email?: string | undefined;
46
+ user_id?: string | undefined;
47
+ }>;
48
+ export type MuseCodeKeyResponse = typeof museCodeKeyResponseSchema.infer;
49
+ declare const museCodeCredentialSchema: import("@oh-my-pi/omptype").FluentType<{
50
+ apiKey: string;
51
+ oauthAccessToken: string;
52
+ }, {
53
+ apiKey: string;
54
+ oauthAccessToken: string;
55
+ }>;
56
+ export type MuseCodeCredential = typeof museCodeCredentialSchema.infer;
57
+ export interface MuseCodeKeyRequestOptions {
58
+ fetch?: FetchImpl;
59
+ signal?: AbortSignal;
60
+ /** Ask Meta to onboard the account during an interactive login exchange. */
61
+ onboard?: boolean;
62
+ }
63
+ export declare function parseMuseCodeCredential(value: string): MuseCodeCredential;
64
+ export declare function requestMuseCodeKey(accessToken: string, options?: MuseCodeKeyRequestOptions): Promise<MuseCodeKeyResponse>;
65
+ /** Exchange Meta account access for the Model API key authorized by a Muse subscription. */
66
+ export declare const attachMuseCodeApiKey: AfterExchangeHook;
67
+ export {};
@@ -0,0 +1,2 @@
1
+ import type { UsageProvider } from "../usage.js";
2
+ export declare const museCodeUsageProvider: UsageProvider;
@@ -478,6 +478,8 @@ export interface UsageProvider {
478
478
  validatesCredentials?: boolean;
479
479
  /** Whether a failed refresh may serve the previous successful report. Defaults to true. */
480
480
  retainLastGoodOnFailure?: boolean;
481
+ /** Provider-specific cool-down after a failed refresh. Defaults to the shared short backoff. */
482
+ failureBackoffMs?: number;
481
483
  }
482
484
  /** Request context used when ranking usage for a specific model. */
483
485
  export interface CredentialRankingContext {
@@ -16,5 +16,7 @@ export interface JsonSchemaValidationResult {
16
16
  success: boolean;
17
17
  issues: JsonSchemaValidationIssue[];
18
18
  }
19
+ /** Whether any same-instance schema declaration or constraint owns this property name. */
20
+ export declare function schemaDefinesProperty(schema: unknown, key: string): boolean;
19
21
  export declare function validateJsonSchemaValue(schema: unknown, value: unknown): JsonSchemaValidationResult;
20
22
  export declare function isJsonSchemaValueValid(schema: unknown, value: unknown): boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-my-pi/pi-ai",
3
- "version": "18.1.11",
3
+ "version": "18.1.12",
4
4
  "description": "Unified LLM API with automatic model discovery and provider configuration",
5
5
  "keywords": [
6
6
  "ai",
@@ -124,11 +124,11 @@
124
124
  "fmt": "oxfmt --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts'"
125
125
  },
126
126
  "dependencies": {
127
- "@oh-my-pi/omptype": "18.1.11",
128
- "@oh-my-pi/pi-catalog": "18.1.11",
129
- "@oh-my-pi/pi-natives": "18.1.11",
130
- "@oh-my-pi/pi-utils": "18.1.11",
131
- "@oh-my-pi/pi-wire": "18.1.11"
127
+ "@oh-my-pi/omptype": "18.1.12",
128
+ "@oh-my-pi/pi-catalog": "18.1.12",
129
+ "@oh-my-pi/pi-natives": "18.1.12",
130
+ "@oh-my-pi/pi-utils": "18.1.12",
131
+ "@oh-my-pi/pi-wire": "18.1.12"
132
132
  },
133
133
  "devDependencies": {
134
134
  "@types/bun": "^1.3.14"
@@ -21,7 +21,7 @@ import type { ApiKeyResolver } from "./auth-retry";
21
21
  import * as AIError from "./error";
22
22
  import { isUsageLimitOutcome } from "./error/rate-limit";
23
23
  import { getProviderDefinition, PASTE_CODE_LOGIN_PROVIDERS } from "./registry";
24
- import { getOAuthApiKey, getOAuthProvider, refreshOAuthToken } from "./registry/oauth";
24
+ import { getOAuthApiKey, getOAuthProvider, normalizeOAuthCredentialExpiry, refreshOAuthToken } from "./registry/oauth";
25
25
  import type {
26
26
  OAuthAuthInfo,
27
27
  OAuthController,
@@ -59,6 +59,7 @@ import { googleGeminiCliUsageProvider } from "./usage/gemini";
59
59
  import { githubCopilotUsageProvider } from "./usage/github-copilot";
60
60
  import { antigravityRankingStrategy, antigravityUsageProvider } from "./usage/google-antigravity";
61
61
  import { kimiRankingStrategy, kimiUsageProvider } from "./usage/kimi";
62
+ import { museCodeUsageProvider } from "./usage/muse-code";
62
63
  import { minimaxCodeUsageProvider } from "./usage/minimax-code";
63
64
  import { ollamaCloudUsageProvider, ollamaUsageProvider } from "./usage/ollama";
64
65
  import { codexRankingStrategy, openaiCodexUsageProvider } from "./usage/openai-codex";
@@ -661,6 +662,7 @@ const DEFAULT_USAGE_PROVIDERS: UsageProvider[] = [
661
662
  openaiCodexUsageProvider,
662
663
  kimiUsageProvider,
663
664
  minimaxCodeUsageProvider,
665
+ museCodeUsageProvider,
664
666
  antigravityUsageProvider,
665
667
  googleGeminiCliUsageProvider,
666
668
  ollamaUsageProvider,
@@ -3601,13 +3603,14 @@ export class AuthStorage {
3601
3603
  // re-hit the endpoint on every poll. Most providers serve the last good
3602
3604
  // value through transient failures. Session-cookie providers can opt out
3603
3605
  // so an expired login does not display stale quota indefinitely.
3604
- const retainLastGood =
3605
- !forceRefresh && this.#resolveUsageProvider(request.provider)?.retainLastGoodOnFailure !== false;
3606
+ const providerImpl = this.#resolveUsageProvider(request.provider);
3607
+ const retainLastGood = !forceRefresh && providerImpl?.retainLastGoodOnFailure !== false;
3606
3608
  const lastGood = retainLastGood
3607
3609
  ? (this.#usageCache.getStale<UsageReport | null>(cacheKey)?.value ?? null)
3608
3610
  : null;
3609
- const backoffJitter = USAGE_FAILURE_BACKOFF_MS * (Math.random() * 0.5 - 0.25);
3610
- const coolDown = Date.now() + USAGE_FAILURE_BACKOFF_MS + backoffJitter;
3611
+ const failureBackoffMs = providerImpl?.failureBackoffMs ?? USAGE_FAILURE_BACKOFF_MS;
3612
+ const backoffJitter = failureBackoffMs * (Math.random() * 0.5 - 0.25);
3613
+ const coolDown = Date.now() + failureBackoffMs + backoffJitter;
3611
3614
  this.#usageCache.set(cacheKey, { value: lastGood, expiresAt: coolDown });
3612
3615
  return lastGood;
3613
3616
  })().finally(() => {
@@ -5484,6 +5487,7 @@ export class AuthStorage {
5484
5487
  credentialId: number | undefined,
5485
5488
  signal?: AbortSignal,
5486
5489
  ): Promise<OAuthCredentials> {
5490
+ credential = normalizeOAuthCredentialExpiry(provider, credential);
5487
5491
  if (credentialId !== undefined) {
5488
5492
  const existing = this.#oauthCredentialRefreshInFlight.get(credentialId);
5489
5493
  if (existing) return raceCredentialRefreshWithSignal(existing, signal);
@@ -24,6 +24,8 @@ export type OAuthErrorKind =
24
24
  | "configuration"
25
25
  /** Cloud project provisioning / onboarding (loadCodeAssist, onboardUser). */
26
26
  | "provisioning"
27
+ /** Subscription/payment gate: plan required or purchase action needed. */
28
+ | "entitlement"
27
29
  /** OIDC / endpoint discovery failed. */
28
30
  | "discovery";
29
31
 
package/src/index.ts CHANGED
@@ -35,6 +35,7 @@ export * from "./usage/github-copilot";
35
35
  export * from "./usage/google-antigravity";
36
36
  export * from "./usage/kimi";
37
37
  export * from "./usage/minimax-code";
38
+ export * from "./usage/muse-code";
38
39
  export * from "./usage/ollama";
39
40
  export * from "./usage/openai-codex";
40
41
  export * from "./usage/openai-codex-reset";
@@ -1238,12 +1238,13 @@ export function resolveOpenAICompletionsOutputClamp(
1238
1238
  /**
1239
1239
  * Provider-specific Responses API output clamp.
1240
1240
  *
1241
- * Meta documents a 131,072-token output limit for Muse Spark 1.1, so native
1242
- * Meta requests may use the model's full advertised cap instead of the
1243
- * conservative 64k OpenAI-compatible default.
1241
+ * Models whose compiled provider policy opts in may use their full advertised
1242
+ * output cap instead of the conservative 64k OpenAI-compatible default.
1244
1243
  */
1245
- export function resolveOpenAIResponsesOutputClamp(model: Pick<Model, "provider" | "maxTokens">): number | undefined {
1246
- if (model.provider === "meta") {
1244
+ export function resolveOpenAIResponsesOutputClamp(
1245
+ model: Pick<Model, "maxTokens"> & { compat: Pick<ResolvedOpenAISharedCompat, "clampOutputToModelMax"> },
1246
+ ): number | undefined {
1247
+ if (model.compat.clampOutputToModelMax) {
1247
1248
  return model.maxTokens ?? OPENAI_MAX_OUTPUT_TOKENS;
1248
1249
  }
1249
1250
  return undefined;
@@ -3573,7 +3574,10 @@ export function applyCommonResponsesSamplingParams<P extends CommonResponsesPara
3573
3574
  params: P,
3574
3575
  options: CommonSamplingOptions | undefined,
3575
3576
  model: Pick<Model, "provider" | "api" | "id" | "omitMaxOutputTokens" | "maxTokens" | "identity"> & {
3576
- compat: Pick<ResolvedOpenAISharedCompat, "supportsSamplingParams" | "supportsPenaltyAndStopParams">;
3577
+ compat: Pick<
3578
+ ResolvedOpenAISharedCompat,
3579
+ "supportsSamplingParams" | "supportsPenaltyAndStopParams" | "clampOutputToModelMax"
3580
+ >;
3577
3581
  },
3578
3582
  ): void {
3579
3583
  if (options?.maxTokens && !model.omitMaxOutputTokens) {
@@ -158,16 +158,20 @@ export function mapCredentials(
158
158
  break;
159
159
  case "seconds": {
160
160
  const seconds = jsonPath(body, expiry.path);
161
- if (typeof seconds !== "number" || !Number.isFinite(seconds)) {
162
- throw new AIError.OAuthError(`${provider} token response missing ${expiry.path}`, {
163
- kind: "validation",
164
- provider,
165
- });
161
+ if (typeof seconds === "number" && Number.isFinite(seconds)) {
162
+ const from = expiry.fromPath ? jsonPath(body, expiry.fromPath) : undefined;
163
+ const base = typeof from === "number" && Number.isFinite(from) ? from * 1000 : Date.now();
164
+ expires = base + seconds * 1000 - expiry.skewMs;
165
+ break;
166
166
  }
167
- const from = expiry.fromPath ? jsonPath(body, expiry.fromPath) : undefined;
168
- const base = typeof from === "number" && Number.isFinite(from) ? from * 1000 : Date.now();
169
- expires = base + seconds * 1000 - expiry.skewMs;
170
- break;
167
+ if (expiry.fallbackMs !== undefined) {
168
+ expires = Date.now() + expiry.fallbackMs;
169
+ break;
170
+ }
171
+ throw new AIError.OAuthError(`${provider} token response missing ${expiry.path}`, {
172
+ kind: "validation",
173
+ provider,
174
+ });
171
175
  }
172
176
  }
173
177
  const credentials: OAuthCredentials = {
@@ -10,4 +10,6 @@ export const DEVICE_CODE_HEADERS_HOOKS: Record<string, Lazy<HeadersHook>> = {
10
10
  export const DEVICE_CODE_VALUE_HOOKS: Record<string, Lazy<ValueHook>> = {
11
11
  "xai-token-endpoint": () => import("../oauth/xai-oauth").then(module => module.getXAITokenEndpoint),
12
12
  };
13
- export const DEVICE_CODE_AFTER_EXCHANGE_HOOKS: Record<string, Lazy<AfterExchangeHook>> = {};
13
+ export const DEVICE_CODE_AFTER_EXCHANGE_HOOKS: Record<string, Lazy<AfterExchangeHook>> = {
14
+ "muse-code-key": () => import("../oauth/muse-code").then(module => module.attachMuseCodeApiKey),
15
+ };
@@ -0,0 +1,23 @@
1
+ import { parseMuseCodeCredential } from "./oauth/muse-code";
2
+ import type { ProviderTransport } from "./build";
3
+
4
+ /** Muse stores both the Meta account token and its subscription-minted Model API key in one OAuth bearer. */
5
+ export const museCodeTransport: ProviderTransport = {
6
+ prepareRequest: (model, options) => {
7
+ if (!options.apiKey) return { model, options };
8
+ const { apiKey } = parseMuseCodeCredential(options.apiKey);
9
+ return {
10
+ model,
11
+ options: {
12
+ ...options,
13
+ apiKey,
14
+ headers: { "x-api-version": "1.0.0", ...options.headers },
15
+ },
16
+ };
17
+ },
18
+ prepareModelDiscovery: config => {
19
+ if (!config.apiKey) return { ...config, authenticated: false };
20
+ const { apiKey } = parseMuseCodeCredential(config.apiKey);
21
+ return { ...config, apiKey, authenticated: true };
22
+ },
23
+ };
@@ -87,7 +87,19 @@ export async function refreshOAuthToken(
87
87
  return def.refreshToken ? def.refreshToken(credentials, signal) : credentials;
88
88
  }
89
89
  const JWT_EXPIRY_SKEW_MS = 5 * 60_000;
90
+ export function normalizeOAuthCredentialExpiry<T extends OAuthCredentials>(provider: string, credentials: T): T {
91
+ if (authPolicyFor(provider)?.expiry !== "jwt-or-never") return credentials;
90
92
 
93
+ // Trust a JWT expiry claim when present; otherwise treat providers with
94
+ // non-expiring sessions as such rather than honoring stale stored expiry
95
+ // timestamps written by older login implementations.
96
+ const normalizedExpires =
97
+ credentials.expires > 0 && credentials.expires < 10_000_000_000
98
+ ? credentials.expires * 1000
99
+ : credentials.expires;
100
+ const expires = jwtExpiryMs(credentials.access, JWT_EXPIRY_SKEW_MS) ?? Math.max(normalizedExpires, NEVER_EXPIRES);
101
+ return expires === credentials.expires ? credentials : ({ ...credentials, expires } as T);
102
+ }
91
103
  /**
92
104
  * Build API-key bytes for a provider from an already-fresh OAuth credential.
93
105
  *
@@ -110,20 +122,7 @@ export async function getOAuthApiKey(
110
122
  }
111
123
 
112
124
  const policy = authPolicyFor(provider);
113
- const jwtOrNever = policy?.expiry === "jwt-or-never";
114
- if (jwtOrNever) {
115
- // Session JWTs (Perplexity) usually omit `exp` (server-side sessions).
116
- // Trust the JWT claim when present; otherwise treat the credential as
117
- // non-expiring rather than honoring a stale stored `expires` (older
118
- // logins wrote loginTime+1h).
119
- const normalizedExpires =
120
- creds.expires > 0 && creds.expires < 10_000_000_000 ? creds.expires * 1000 : creds.expires;
121
- const jwtExpiry = jwtExpiryMs(creds.access, JWT_EXPIRY_SKEW_MS);
122
- const expires = jwtExpiry ?? Math.max(normalizedExpires, NEVER_EXPIRES);
123
- if (expires !== creds.expires) {
124
- creds = { ...creds, expires };
125
- }
126
- }
125
+ creds = normalizeOAuthCredentialExpiry(provider, creds);
127
126
  // Refresh is the sole responsibility of `AuthStorage` (which calls
128
127
  // `refreshOAuthToken` directly with broker-aware single-flighting). If we
129
128
  // reach here with an expired credential, the outer pipeline failed to
@@ -132,13 +131,6 @@ export async function getOAuthApiKey(
132
131
  // trigger a `__remote__`-against-real-provider failure that gets classified
133
132
  // as `invalid_grant` and disables the row. Refuse loudly instead.
134
133
  if (Date.now() >= creds.expires) {
135
- if (jwtOrNever) {
136
- const jwtExpiry = jwtExpiryMs(creds.access, JWT_EXPIRY_SKEW_MS);
137
- if (jwtExpiry && Date.now() < jwtExpiry) {
138
- const fallbackCredentials = { ...creds, expires: jwtExpiry };
139
- return { newCredentials: fallbackCredentials, apiKey: fallbackCredentials.access };
140
- }
141
- }
142
134
  throw new AIError.OAuthError(
143
135
  `OAuth credential for ${provider} is expired and must be refreshed via AuthStorage before getOAuthApiKey is called`,
144
136
  { kind: "validation", provider },
@@ -0,0 +1,174 @@
1
+ import { type } from "@oh-my-pi/omptype";
2
+ import * as AIError from "../../error";
3
+ import type { AfterExchangeHook, ExchangeContext } from "../hooks/types";
4
+ import type { FetchImpl } from "../../types";
5
+ import type { OAuthCredentials } from "./types";
6
+
7
+ const PROVIDER = "muse-code";
8
+ const MUSE_KEY_URL = "https://api.meta.ai/muse-code/key";
9
+ const API_VERSION = "1.0.0";
10
+ const REQUEST_TIMEOUT_MS = 20_000;
11
+
12
+ const subscriptionWindowSchema = type({
13
+ "used_percent?": "number",
14
+ "resets_at?": "string | number",
15
+ "window_duration_mins?": "number",
16
+ });
17
+
18
+ const subscriptionUsageSchema = type({
19
+ "window?": subscriptionWindowSchema.or("null"),
20
+ "weekly?": subscriptionWindowSchema.or("null"),
21
+ });
22
+
23
+ const museCodeKeyResponseSchema = type({
24
+ "api_key?": "string",
25
+ "require_payment_action_url?": "string",
26
+ "require_payment?": "boolean",
27
+ "action_url?": "string | null",
28
+ "user_email?": "string",
29
+ "user_id?": "string",
30
+ "is_subs_active?": "boolean",
31
+ "subs_tier_id?": "string",
32
+ "subs_tier_name?": "string",
33
+ "subs_usage?": subscriptionUsageSchema.or("null"),
34
+ });
35
+ export type MuseCodeKeyResponse = typeof museCodeKeyResponseSchema.infer;
36
+
37
+ const museCodeCredentialSchema = type({
38
+ oauthAccessToken: "string",
39
+ apiKey: "string",
40
+ });
41
+ export type MuseCodeCredential = typeof museCodeCredentialSchema.infer;
42
+ export interface MuseCodeKeyRequestOptions {
43
+ fetch?: FetchImpl;
44
+ signal?: AbortSignal;
45
+ /** Ask Meta to onboard the account during an interactive login exchange. */
46
+ onboard?: boolean;
47
+ }
48
+
49
+ function requestSignal(signal?: AbortSignal): AbortSignal {
50
+ const timeout = AbortSignal.timeout(REQUEST_TIMEOUT_MS);
51
+ return signal ? AbortSignal.any([signal, timeout]) : timeout;
52
+ }
53
+
54
+ export function parseMuseCodeCredential(value: string): MuseCodeCredential {
55
+ let payload: unknown;
56
+ try {
57
+ payload = JSON.parse(value);
58
+ } catch (cause) {
59
+ throw new AIError.ConfigurationError("Muse Code credential is invalid; sign in again", { cause });
60
+ }
61
+ const parsed = museCodeCredentialSchema(payload);
62
+ if (parsed instanceof type.errors || !parsed.oauthAccessToken.trim() || !parsed.apiKey.trim()) {
63
+ throw new AIError.ConfigurationError("Muse Code credential is invalid; sign in again");
64
+ }
65
+ return parsed;
66
+ }
67
+
68
+ function encodeMuseCodeCredential(oauthAccessToken: string, apiKey: string): string {
69
+ return JSON.stringify({ oauthAccessToken, apiKey });
70
+ }
71
+
72
+ export async function requestMuseCodeKey(
73
+ accessToken: string,
74
+ options: MuseCodeKeyRequestOptions = {},
75
+ ): Promise<MuseCodeKeyResponse> {
76
+ const response = await (options.fetch ?? fetch)(MUSE_KEY_URL, {
77
+ method: "POST",
78
+ headers: {
79
+ Accept: "application/json",
80
+ Authorization: `Bearer ${accessToken}`,
81
+ "Content-Type": "application/json",
82
+ "x-api-version": API_VERSION,
83
+ },
84
+ body: JSON.stringify(options.onboard ? { onboard: true } : {}),
85
+ redirect: "error",
86
+ signal: requestSignal(options.signal),
87
+ });
88
+ const text = await response.text();
89
+ if (!response.ok) {
90
+ const excerpt = text.trim() ? ` ${text.slice(0, 500).trim()}` : "";
91
+ throw new AIError.OAuthError(`Muse Code key exchange failed: ${response.status}${excerpt}`, {
92
+ kind: "token-exchange",
93
+ provider: PROVIDER,
94
+ status: response.status,
95
+ });
96
+ }
97
+ let payload: unknown;
98
+ try {
99
+ payload = JSON.parse(text);
100
+ } catch (cause) {
101
+ throw new AIError.OAuthError("Muse Code key exchange returned invalid JSON", {
102
+ kind: "validation",
103
+ provider: PROVIDER,
104
+ status: response.status,
105
+ cause,
106
+ });
107
+ }
108
+ const parsed = museCodeKeyResponseSchema(payload);
109
+ if (parsed instanceof type.errors) {
110
+ throw new AIError.OAuthError(`Invalid Muse Code key response: ${parsed.summary}`, {
111
+ kind: "validation",
112
+ provider: PROVIDER,
113
+ });
114
+ }
115
+ return parsed;
116
+ }
117
+
118
+ /** Exchange Meta account access for the Model API key authorized by a Muse subscription. */
119
+ export const attachMuseCodeApiKey: AfterExchangeHook = async (
120
+ credentials: OAuthCredentials,
121
+ context: ExchangeContext,
122
+ ): Promise<OAuthCredentials> => {
123
+ // Reuse an already-minted subscription key instead of re-minting on every
124
+ // token refresh. The key endpoint is aggressively rate-limited (429s), and
125
+ // Meta returns the same api_key for the account, so a refresh that already
126
+ // carries one must not burn another key call. Only a login/refresh without
127
+ // one (first login, or a previously failed mint) re-mints here.
128
+ try {
129
+ const existing = parseMuseCodeCredential(credentials.access);
130
+ if (existing.apiKey.trim()) return credentials;
131
+ } catch {
132
+ // No usable minted key yet — fall through to mint one.
133
+ }
134
+ const payload = await requestMuseCodeKey(credentials.access, {
135
+ fetch: context.fetch,
136
+ signal: context.signal,
137
+ onboard: true,
138
+ });
139
+ if (payload.is_subs_active === false) {
140
+ throw new AIError.OAuthError("invalid_grant: Muse Code subscription is inactive", {
141
+ kind: "token-exchange",
142
+ provider: PROVIDER,
143
+ status: 403,
144
+ });
145
+ }
146
+ const apiKey = payload.api_key?.trim() || "";
147
+ if (!apiKey) {
148
+ const actionUrl = payload.action_url?.trim() || payload.require_payment_action_url?.trim();
149
+ if (payload.require_payment === true || actionUrl) {
150
+ throw new AIError.OAuthError(
151
+ actionUrl ? `Muse Code subscription is required: ${actionUrl}` : "Muse Code subscription is required",
152
+ { kind: "entitlement", provider: PROVIDER },
153
+ );
154
+ }
155
+ throw new AIError.OAuthError("Muse Code key response is missing api_key", {
156
+ kind: "validation",
157
+ provider: PROVIDER,
158
+ });
159
+ }
160
+ const email = payload.user_email?.trim().toLowerCase();
161
+ const accountId = payload.user_id?.trim() || email;
162
+ if (!accountId) {
163
+ throw new AIError.OAuthError("Muse Code key response is missing a stable account identity", {
164
+ kind: "validation",
165
+ provider: PROVIDER,
166
+ });
167
+ }
168
+ return {
169
+ ...credentials,
170
+ access: encodeMuseCodeCredential(credentials.access, apiKey),
171
+ accountId,
172
+ email,
173
+ };
174
+ };
@@ -5,6 +5,7 @@ import { amazonBedrockTransport } from "./amazon-bedrock";
5
5
  import { bedrockMantleTransport } from "./bedrock-mantle";
6
6
  import { buildProviderDefinition, type ProviderTransport } from "./build";
7
7
  import { cloudflareAiGatewayTransport } from "./cloudflare-ai-gateway";
8
+ import { museCodeTransport } from "./muse-code";
8
9
  import type { ProviderDefinition } from "./types";
9
10
 
10
11
  /**
@@ -16,6 +17,7 @@ const TRANSPORTS: Record<string, ProviderTransport> = {
16
17
  "amazon-bedrock": amazonBedrockTransport,
17
18
  "bedrock-mantle": bedrockMantleTransport,
18
19
  "cloudflare-ai-gateway": cloudflareAiGatewayTransport,
20
+ "muse-code": museCodeTransport,
19
21
  };
20
22
 
21
23
  /**
@@ -0,0 +1,158 @@
1
+ import * as AIError from "../error";
2
+ import { parseMuseCodeCredential, requestMuseCodeKey, type MuseCodeKeyResponse } from "../registry/oauth/muse-code";
3
+ import type { UsageAmount, UsageFetchParams, UsageLimit, UsageProvider, UsageReport, UsageWindow } from "../usage";
4
+ import { parseIsoTimestamp, parsePositiveTimestamp, usageStatus, WEEK_MS } from "./shared";
5
+
6
+ const PROVIDER = "muse-code";
7
+ const SOURCE = "api.meta.ai/muse-code/key";
8
+ const FAILURE_BACKOFF_MS = 5 * 60_000;
9
+
10
+ function parseResetTimestamp(value: string | number | undefined): number | undefined {
11
+ return typeof value === "string" ? parseIsoTimestamp(value) : parsePositiveTimestamp(value);
12
+ }
13
+
14
+ function percentAmount(usedPercent: number): UsageAmount {
15
+ const used = Math.min(usedPercent, 100);
16
+ const usedFraction = used / 100;
17
+ return {
18
+ used,
19
+ limit: 100,
20
+ remaining: 100 - used,
21
+ usedFraction,
22
+ remainingFraction: 1 - usedFraction,
23
+ unit: "percent",
24
+ };
25
+ }
26
+
27
+ function buildLimit(
28
+ id: string,
29
+ label: string,
30
+ usedPercent: number | undefined,
31
+ window: UsageWindow,
32
+ tier: string | undefined,
33
+ accountId: string | undefined,
34
+ ): UsageLimit | undefined {
35
+ if (typeof usedPercent !== "number" || !Number.isFinite(usedPercent) || usedPercent < 0) return undefined;
36
+ const amount = percentAmount(usedPercent);
37
+ return {
38
+ id,
39
+ label,
40
+ scope: { provider: PROVIDER, accountId, tier, windowId: window.id, shared: true },
41
+ window,
42
+ amount,
43
+ status: usageStatus(amount.usedFraction),
44
+ };
45
+ }
46
+
47
+ function formatRollingWindowLabel(minutes: number | undefined): string {
48
+ if (minutes === undefined) return "Rolling Window";
49
+ if (minutes % 60 === 0) {
50
+ const hours = minutes / 60;
51
+ return `${hours} ${hours === 1 ? "Hour" : "Hours"}`;
52
+ }
53
+ return `${minutes} ${minutes === 1 ? "Minute" : "Minutes"}`;
54
+ }
55
+
56
+ function buildLimits(payload: MuseCodeKeyResponse, accountId: string | undefined): UsageLimit[] {
57
+ const usage = payload.subs_usage;
58
+ if (!usage) return [];
59
+ const tier = payload.subs_tier_name?.trim() || payload.subs_tier_id?.trim() || undefined;
60
+ const limits: UsageLimit[] = [];
61
+ if (usage.window) {
62
+ const minutes = usage.window.window_duration_mins;
63
+ const durationMs =
64
+ typeof minutes === "number" && Number.isFinite(minutes) && minutes > 0 ? minutes * 60_000 : undefined;
65
+ const label = formatRollingWindowLabel(
66
+ typeof minutes === "number" && Number.isFinite(minutes) && minutes > 0 ? Math.round(minutes) : undefined,
67
+ );
68
+ const id = durationMs === undefined ? "rolling" : `${Math.round(durationMs / 60_000)}m`;
69
+ const limit = buildLimit(
70
+ id,
71
+ label,
72
+ usage.window.used_percent,
73
+ {
74
+ id,
75
+ label,
76
+ durationMs,
77
+ resetsAt: parseResetTimestamp(usage.window.resets_at),
78
+ },
79
+ tier,
80
+ accountId,
81
+ );
82
+ if (limit) limits.push(limit);
83
+ }
84
+ if (usage.weekly) {
85
+ const limit = buildLimit(
86
+ "1w",
87
+ "Weekly",
88
+ usage.weekly.used_percent,
89
+ {
90
+ id: "1w",
91
+ label: "Weekly",
92
+ durationMs: WEEK_MS,
93
+ resetsAt: parseResetTimestamp(usage.weekly.resets_at),
94
+ },
95
+ tier,
96
+ accountId,
97
+ );
98
+ if (limit) limits.push(limit);
99
+ }
100
+ return limits;
101
+ }
102
+
103
+ export const museCodeUsageProvider: UsageProvider = {
104
+ id: PROVIDER,
105
+ validatesCredentials: true,
106
+ failureBackoffMs: FAILURE_BACKOFF_MS,
107
+
108
+ supports(params: UsageFetchParams): boolean {
109
+ if (params.provider !== PROVIDER || params.credential.type !== "oauth" || !params.credential.accessToken) {
110
+ return false;
111
+ }
112
+ try {
113
+ parseMuseCodeCredential(params.credential.accessToken);
114
+ return true;
115
+ } catch {
116
+ return false;
117
+ }
118
+ },
119
+
120
+ async fetchUsage(params, ctx): Promise<UsageReport | null> {
121
+ if (params.provider !== PROVIDER || params.credential.type !== "oauth") return null;
122
+ const encodedCredential = params.credential.accessToken?.trim();
123
+ if (!encodedCredential) return null;
124
+ const { oauthAccessToken } = parseMuseCodeCredential(encodedCredential);
125
+
126
+ let payload: MuseCodeKeyResponse;
127
+ try {
128
+ payload = await requestMuseCodeKey(oauthAccessToken, { fetch: ctx.fetch, signal: params.signal });
129
+ } catch (error) {
130
+ if (error instanceof AIError.OAuthError && (error.status === 401 || error.status === 403)) {
131
+ throw new AIError.ProviderHttpError(error.message, error.status, { cause: error });
132
+ }
133
+ return null;
134
+ }
135
+ if (payload.is_subs_active === false) {
136
+ throw new AIError.ProviderHttpError("Muse Code subscription is inactive", 403);
137
+ }
138
+ const email = payload.user_email?.trim().toLowerCase() || params.credential.email;
139
+ const accountId = payload.user_id?.trim() || params.credential.accountId || email;
140
+ const limits = buildLimits(payload, accountId);
141
+ if (limits.length === 0) return null;
142
+ const raw = { ...payload };
143
+ delete raw.api_key;
144
+
145
+ return {
146
+ provider: PROVIDER,
147
+ fetchedAt: Date.now(),
148
+ limits,
149
+ metadata: {
150
+ source: SOURCE,
151
+ ...(email ? { email } : {}),
152
+ ...(accountId ? { accountId } : {}),
153
+ ...(payload.subs_tier_name ? { tier: payload.subs_tier_name } : {}),
154
+ },
155
+ raw,
156
+ };
157
+ },
158
+ };
@@ -1,3 +1,4 @@
1
+ import { USER_AGENT, getInstallId } from "@oh-my-pi/pi-utils";
1
2
  import { ProviderHttpError } from "../error";
2
3
  import type {
3
4
  CredentialRankingStrategy,
@@ -108,6 +109,13 @@ async function fetchOpenCodeGoUsage(params: UsageFetchParams, ctx: UsageFetchCon
108
109
  headers: {
109
110
  accept: "application/json",
110
111
  authorization: `Bearer ${credential.apiKey}`,
112
+ // Background poll outside any conversation: attribute with the
113
+ // stable install id so OpenCode can optimize/service the
114
+ // request (x-opencode-session required from 09/06). Peers
115
+ // (codex/zai) send USER_AGENT here; without it Bun's default
116
+ // UA is what upstream flags as "Bun fetch".
117
+ "User-Agent": USER_AGENT,
118
+ "x-opencode-session": getInstallId(),
111
119
  },
112
120
  signal: params.signal,
113
121
  });
package/src/usage.ts CHANGED
@@ -359,6 +359,8 @@ export interface UsageProvider {
359
359
  validatesCredentials?: boolean;
360
360
  /** Whether a failed refresh may serve the previous successful report. Defaults to true. */
361
361
  retainLastGoodOnFailure?: boolean;
362
+ /** Provider-specific cool-down after a failed refresh. Defaults to the shared short backoff. */
363
+ failureBackoffMs?: number;
362
364
  }
363
365
 
364
366
  /** Request context used when ranking usage for a specific model. */
@@ -173,6 +173,120 @@ function resolveLocalRef(root: unknown, ref: string): unknown | undefined {
173
173
  return current;
174
174
  }
175
175
 
176
+ /** A presence-only constraint, as distinct from restrictions on a property's value. */
177
+ function requiresOnlyPropertyPresence(schema: unknown, key: string, root: unknown, depth = 0): boolean {
178
+ if (!isJsonObject(schema) || depth >= MAX_REF_DEPTH) return false;
179
+ for (const keyword of Object.keys(schema)) {
180
+ switch (keyword) {
181
+ case "required":
182
+ case "type":
183
+ case "$ref":
184
+ case "description":
185
+ case "title":
186
+ case "$comment":
187
+ break;
188
+ default:
189
+ return false;
190
+ }
191
+ }
192
+ if ("$ref" in schema) {
193
+ if (typeof schema.$ref !== "string" || schema.required !== undefined || schema.type !== undefined) return false;
194
+ return requiresOnlyPropertyPresence(resolveLocalRef(root, schema.$ref), key, root, depth + 1);
195
+ }
196
+ return (
197
+ (schema.type === undefined || schema.type === "object") &&
198
+ Array.isArray(schema.required) &&
199
+ schema.required.includes(key) &&
200
+ schema.required.every(entry => typeof entry === "string")
201
+ );
202
+ }
203
+
204
+ /** Whether any same-instance schema declaration or constraint owns this property name. */
205
+ export function schemaDefinesProperty(schema: unknown, key: string): boolean {
206
+ const root = schema;
207
+ const visited = new WeakMap<object, number>();
208
+ const visit = (node: unknown, context: "positive" | "negative" | "predicate" = "positive"): boolean => {
209
+ if (!isJsonObject(node)) return false;
210
+ const contextBit = context === "positive" ? 1 : context === "negative" ? 2 : 4;
211
+ const previousContexts = visited.get(node) ?? 0;
212
+ if (previousContexts & contextBit) return false;
213
+ visited.set(node, previousContexts | contextBit);
214
+ if (isJsonObject(node.const) && Object.hasOwn(node.const, key)) return true;
215
+ if (Array.isArray(node.enum) && node.enum.some(value => isJsonObject(value) && Object.hasOwn(value, key))) {
216
+ return true;
217
+ }
218
+ const properties = node.properties;
219
+ if (isJsonObject(properties) && Object.hasOwn(properties, key)) {
220
+ return properties[key] !== false || context !== "positive";
221
+ }
222
+ const required = node.required;
223
+ if (Array.isArray(required) && required.includes(key)) return true;
224
+ if (
225
+ node.propertyNames !== undefined &&
226
+ node.additionalProperties !== false &&
227
+ validateSchemaValueInRoot(node.propertyNames, key, root).success
228
+ ) {
229
+ return true;
230
+ }
231
+ const patternProperties = node.patternProperties;
232
+ if (isJsonObject(patternProperties)) {
233
+ for (const [pattern, patternSchema] of Object.entries(patternProperties)) {
234
+ try {
235
+ if (new RegExp(pattern).test(key)) {
236
+ if (patternSchema !== false) return true;
237
+ }
238
+ } catch {
239
+ // Invalid patterns are rejected by validation and cannot declare ownership.
240
+ }
241
+ }
242
+ }
243
+ if (isJsonObject(node.unevaluatedProperties)) return true;
244
+ if (isJsonObject(node.additionalProperties)) return true;
245
+ for (const keyword of ["anyOf", "oneOf", "allOf"] as const) {
246
+ const branches = node[keyword];
247
+ if (Array.isArray(branches) && branches.some(branch => visit(branch, context))) return true;
248
+ }
249
+ if (visit(node.if, "predicate")) return true;
250
+ for (const keyword of ["then", "else"] as const) {
251
+ if (visit(node[keyword], context)) return true;
252
+ }
253
+ // `not: { required: [key] }` forbids the name rather than declaring data.
254
+ // Negated value constraints still own their inputs and must be validated.
255
+ if (
256
+ (context !== "positive" || !requiresOnlyPropertyPresence(node.not, key, root)) &&
257
+ visit(node.not, context === "predicate" ? "predicate" : context === "positive" ? "negative" : "positive")
258
+ ) {
259
+ return true;
260
+ }
261
+ const dependentSchemas = node.dependentSchemas;
262
+ if (isJsonObject(dependentSchemas)) {
263
+ if (Object.hasOwn(dependentSchemas, key)) return true;
264
+ if (Object.values(dependentSchemas).some(dependency => visit(dependency, context))) return true;
265
+ }
266
+ const dependentRequired = node.dependentRequired;
267
+ if (isJsonObject(dependentRequired)) {
268
+ if (Object.hasOwn(dependentRequired, key)) return true;
269
+ for (const dependencies of Object.values(dependentRequired)) {
270
+ if (Array.isArray(dependencies) && dependencies.includes(key)) return true;
271
+ }
272
+ }
273
+ const dependencies = node.dependencies;
274
+ if (isJsonObject(dependencies)) {
275
+ if (Object.hasOwn(dependencies, key)) return true;
276
+ for (const dependency of Object.values(dependencies)) {
277
+ if (Array.isArray(dependency) && dependency.includes(key)) return true;
278
+ if (visit(dependency, context)) return true;
279
+ }
280
+ }
281
+ if (typeof node.$ref === "string") {
282
+ const resolved = resolveLocalRef(root, node.$ref);
283
+ if (resolved !== undefined && visit(resolved, context)) return true;
284
+ }
285
+ return false;
286
+ };
287
+ return visit(schema);
288
+ }
289
+
176
290
  /** Resolve a `#/path/to/node` pointer against the root schema. Returns `undefined` for external/unsupported refs. */
177
291
 
178
292
  function isRequiredSet(value: unknown): value is string[] {
@@ -615,18 +729,22 @@ function validateNumberKeywords(
615
729
  return valid;
616
730
  }
617
731
 
618
- export function validateJsonSchemaValue(schema: unknown, value: unknown): JsonSchemaValidationResult {
732
+ function validateSchemaValueInRoot(schema: unknown, value: unknown, root: unknown): JsonSchemaValidationResult {
619
733
  const issues: JsonSchemaValidationIssue[] = [];
620
734
  const success = validateSchemaNode(
621
735
  schema,
622
736
  value,
623
737
  [],
624
- { root: schema, seenPairs: new Set(), objectIds: new WeakMap(), nextObjectId: { value: 0 }, refDepth: 0 },
738
+ { root, seenPairs: new Set(), objectIds: new WeakMap(), nextObjectId: { value: 0 }, refDepth: 0 },
625
739
  issues,
626
740
  );
627
741
  return { success, issues };
628
742
  }
629
743
 
744
+ export function validateJsonSchemaValue(schema: unknown, value: unknown): JsonSchemaValidationResult {
745
+ return validateSchemaValueInRoot(schema, value, schema);
746
+ }
747
+
630
748
  export function isJsonSchemaValueValid(schema: unknown, value: unknown): boolean {
631
749
  return validateJsonSchemaValue(schema, value).success;
632
750
  }