@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 +7 -0
- package/dist/types/error/oauth.d.ts +2 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/providers/openai-shared.d.ts +6 -5
- package/dist/types/registry/muse-code.d.ts +3 -0
- package/dist/types/registry/oauth/index.d.ts +1 -0
- package/dist/types/registry/oauth/muse-code.d.ts +67 -0
- package/dist/types/usage/muse-code.d.ts +2 -0
- package/dist/types/usage.d.ts +2 -0
- package/dist/types/utils/schema/json-schema-validator.d.ts +2 -0
- package/package.json +6 -6
- package/src/auth-storage.ts +9 -5
- package/src/error/oauth.ts +2 -0
- package/src/index.ts +1 -0
- package/src/providers/openai-shared.ts +10 -6
- package/src/registry/engine/common.ts +13 -9
- package/src/registry/hooks/device-code.ts +3 -1
- package/src/registry/muse-code.ts +23 -0
- package/src/registry/oauth/index.ts +13 -21
- package/src/registry/oauth/muse-code.ts +174 -0
- package/src/registry/registry.ts +2 -0
- package/src/usage/muse-code.ts +158 -0
- package/src/usage/opencode-go.ts +8 -0
- package/src/usage.ts +2 -0
- package/src/utils/schema/json-schema-validator.ts +120 -2
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 {
|
package/dist/types/index.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
361
|
-
*
|
|
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, "
|
|
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;
|
|
@@ -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 {};
|
package/dist/types/usage.d.ts
CHANGED
|
@@ -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.
|
|
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.
|
|
128
|
-
"@oh-my-pi/pi-catalog": "18.1.
|
|
129
|
-
"@oh-my-pi/pi-natives": "18.1.
|
|
130
|
-
"@oh-my-pi/pi-utils": "18.1.
|
|
131
|
-
"@oh-my-pi/pi-wire": "18.1.
|
|
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"
|
package/src/auth-storage.ts
CHANGED
|
@@ -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
|
|
3605
|
-
|
|
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
|
|
3610
|
-
const
|
|
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);
|
package/src/error/oauth.ts
CHANGED
|
@@ -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
|
-
*
|
|
1242
|
-
*
|
|
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(
|
|
1246
|
-
|
|
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<
|
|
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
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
|
|
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
|
+
};
|
package/src/registry/registry.ts
CHANGED
|
@@ -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
|
+
};
|
package/src/usage/opencode-go.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
}
|