Merge public-gh/master into paperclip-company-import-export
This commit is contained in:
28
packages/adapter-utils/src/billing.test.ts
Normal file
28
packages/adapter-utils/src/billing.test.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { inferOpenAiCompatibleBiller } from "./billing.js";
|
||||
|
||||
describe("inferOpenAiCompatibleBiller", () => {
|
||||
it("returns openrouter when OPENROUTER_API_KEY is present", () => {
|
||||
expect(
|
||||
inferOpenAiCompatibleBiller({ OPENROUTER_API_KEY: "sk-or-123" } as NodeJS.ProcessEnv, "openai"),
|
||||
).toBe("openrouter");
|
||||
});
|
||||
|
||||
it("returns openrouter when OPENAI_BASE_URL points at OpenRouter", () => {
|
||||
expect(
|
||||
inferOpenAiCompatibleBiller(
|
||||
{ OPENAI_BASE_URL: "https://openrouter.ai/api/v1" } as NodeJS.ProcessEnv,
|
||||
"openai",
|
||||
),
|
||||
).toBe("openrouter");
|
||||
});
|
||||
|
||||
it("returns fallback when no OpenRouter markers are present", () => {
|
||||
expect(
|
||||
inferOpenAiCompatibleBiller(
|
||||
{ OPENAI_BASE_URL: "https://api.openai.com/v1" } as NodeJS.ProcessEnv,
|
||||
"openai",
|
||||
),
|
||||
).toBe("openai");
|
||||
});
|
||||
});
|
||||
20
packages/adapter-utils/src/billing.ts
Normal file
20
packages/adapter-utils/src/billing.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
function readEnv(env: NodeJS.ProcessEnv, key: string): string | null {
|
||||
const value = env[key];
|
||||
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
||||
}
|
||||
|
||||
export function inferOpenAiCompatibleBiller(
|
||||
env: NodeJS.ProcessEnv,
|
||||
fallback: string | null = "openai",
|
||||
): string | null {
|
||||
const explicitOpenRouterKey = readEnv(env, "OPENROUTER_API_KEY");
|
||||
if (explicitOpenRouterKey) return "openrouter";
|
||||
|
||||
const baseUrl =
|
||||
readEnv(env, "OPENAI_BASE_URL") ??
|
||||
readEnv(env, "OPENAI_API_BASE") ??
|
||||
readEnv(env, "OPENAI_API_BASE_URL");
|
||||
if (baseUrl && /openrouter\.ai/i.test(baseUrl)) return "openrouter";
|
||||
|
||||
return fallback;
|
||||
}
|
||||
@@ -22,6 +22,8 @@ export type {
|
||||
HireApprovedPayload,
|
||||
HireApprovedHookResult,
|
||||
ServerAdapterModule,
|
||||
QuotaWindow,
|
||||
ProviderQuotaResult,
|
||||
TranscriptEntry,
|
||||
StdoutLineParser,
|
||||
CLIAdapterModule,
|
||||
@@ -33,3 +35,4 @@ export {
|
||||
redactHomePathUserSegmentsInValue,
|
||||
redactTranscriptEntryPaths,
|
||||
} from "./log-redaction.js";
|
||||
export { inferOpenAiCompatibleBiller } from "./billing.js";
|
||||
|
||||
@@ -30,7 +30,15 @@ export interface UsageSummary {
|
||||
cachedInputTokens?: number;
|
||||
}
|
||||
|
||||
export type AdapterBillingType = "api" | "subscription" | "unknown";
|
||||
export type AdapterBillingType =
|
||||
| "api"
|
||||
| "subscription"
|
||||
| "metered_api"
|
||||
| "subscription_included"
|
||||
| "subscription_overage"
|
||||
| "credits"
|
||||
| "fixed"
|
||||
| "unknown";
|
||||
|
||||
export interface AdapterRuntimeServiceReport {
|
||||
id?: string | null;
|
||||
@@ -68,6 +76,7 @@ export interface AdapterExecutionResult {
|
||||
sessionParams?: Record<string, unknown> | null;
|
||||
sessionDisplayId?: string | null;
|
||||
provider?: string | null;
|
||||
biller?: string | null;
|
||||
model?: string | null;
|
||||
billingType?: AdapterBillingType | null;
|
||||
costUsd?: number | null;
|
||||
@@ -209,6 +218,37 @@ export interface HireApprovedHookResult {
|
||||
detail?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Quota window types — used by adapters that can report provider quota/rate-limit state
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** a single rate-limit or usage window returned by a provider quota API */
|
||||
export interface QuotaWindow {
|
||||
/** human label, e.g. "5h", "7d", "Sonnet 7d", "Credits" */
|
||||
label: string;
|
||||
/** percent of the window already consumed (0-100), null when not reported */
|
||||
usedPercent: number | null;
|
||||
/** iso timestamp when this window resets, null when not reported */
|
||||
resetsAt: string | null;
|
||||
/** free-form value label for credit-style windows, e.g. "$4.20 remaining" */
|
||||
valueLabel: string | null;
|
||||
/** optional supporting text, e.g. reset details or provider-specific notes */
|
||||
detail?: string | null;
|
||||
}
|
||||
|
||||
/** result for one provider from getQuotaWindows() */
|
||||
export interface ProviderQuotaResult {
|
||||
/** provider slug, e.g. "anthropic", "openai" */
|
||||
provider: string;
|
||||
/** source label when the provider reports where the quota data came from */
|
||||
source?: string | null;
|
||||
/** true when the fetch succeeded and windows is populated */
|
||||
ok: boolean;
|
||||
/** error message when ok is false */
|
||||
error?: string;
|
||||
windows: QuotaWindow[];
|
||||
}
|
||||
|
||||
export interface ServerAdapterModule {
|
||||
type: string;
|
||||
execute(ctx: AdapterExecutionContext): Promise<AdapterExecutionResult>;
|
||||
@@ -228,6 +268,12 @@ export interface ServerAdapterModule {
|
||||
payload: HireApprovedPayload,
|
||||
adapterConfig: Record<string, unknown>,
|
||||
) => Promise<HireApprovedHookResult>;
|
||||
/**
|
||||
* Optional: fetch live provider quota/rate-limit windows for this adapter.
|
||||
* Returns a ProviderQuotaResult so the server can aggregate across adapters
|
||||
* without knowing provider-specific credential paths or API shapes.
|
||||
*/
|
||||
getQuotaWindows?: () => Promise<ProviderQuotaResult>;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user