199 lines
5.9 KiB
TypeScript
199 lines
5.9 KiB
TypeScript
import { sdk } from '../sdk'
|
|
import { configFile } from '../file-models/config.json'
|
|
|
|
const { InputSpec, Value, Variants } = sdk
|
|
|
|
// Operator-facing knob for tier-quota tuning without a code change or
|
|
// redeploy. The schema is { core: TierConfig, pro: TierConfig,
|
|
// max: TierConfig } where TierConfig fields can be number or null.
|
|
// `null` means "no cap on this dimension." The relay reads this on
|
|
// every request via configFile's live-reload.
|
|
|
|
// Max-tier monthly credits is rendered as a union — operator can pick
|
|
// "unlimited" (the default) or specify a hard monthly cap. Surfaced
|
|
// as a Value.union so the StartOS UI gives a clean radio-style choice
|
|
// rather than relying on a sentinel like 0 = unlimited.
|
|
const maxMonthlyVariants = Variants.of({
|
|
unlimited: {
|
|
name: 'Unlimited',
|
|
spec: InputSpec.of({}),
|
|
},
|
|
limited: {
|
|
name: 'Limit to a specific amount',
|
|
spec: InputSpec.of({
|
|
amount: Value.number({
|
|
name: 'Monthly Limit',
|
|
description:
|
|
'Total credits each Max-tier install gets per calendar month.',
|
|
required: true,
|
|
default: 1000,
|
|
min: 0,
|
|
max: 1_000_000,
|
|
integer: true,
|
|
step: 1,
|
|
units: 'credits',
|
|
placeholder: null,
|
|
}),
|
|
}),
|
|
},
|
|
})
|
|
|
|
const inputSpec = InputSpec.of({
|
|
// Core tier knobs.
|
|
core_lifetime: Value.number({
|
|
name: 'Core — Lifetime Credits',
|
|
description:
|
|
'Total credits a Core (unlicensed) install can ever spend across its lifetime. Default 10 (5 served via Gemini + 5 via operator hardware).',
|
|
required: true,
|
|
default: 10,
|
|
min: 0,
|
|
max: 1_000_000,
|
|
integer: true,
|
|
step: 1,
|
|
units: 'credits',
|
|
placeholder: null,
|
|
}),
|
|
core_gemini_cap: Value.number({
|
|
name: 'Core — Gemini Cap (lifetime)',
|
|
description:
|
|
'Within the Core lifetime allowance, how many credits may be served via Gemini (the rest spill to the operator-hardware fallback). Default 5.',
|
|
required: true,
|
|
default: 5,
|
|
min: 0,
|
|
max: 1_000_000,
|
|
integer: true,
|
|
step: 1,
|
|
units: 'credits',
|
|
placeholder: null,
|
|
}),
|
|
// Pro tier knobs.
|
|
pro_monthly: Value.number({
|
|
name: 'Pro — Monthly Credits',
|
|
description:
|
|
'Total credits a Pro user gets each calendar month. Resets on the 1st. Default 50.',
|
|
required: true,
|
|
default: 50,
|
|
min: 0,
|
|
max: 1_000_000,
|
|
integer: true,
|
|
step: 1,
|
|
units: 'credits',
|
|
placeholder: null,
|
|
}),
|
|
pro_gemini_cap: Value.number({
|
|
name: 'Pro — Gemini Cap (monthly)',
|
|
description:
|
|
'Within the Pro monthly allowance, how many credits may be served via Gemini (the rest spill to the operator-hardware fallback). Default 25.',
|
|
required: true,
|
|
default: 25,
|
|
min: 0,
|
|
max: 1_000_000,
|
|
integer: true,
|
|
step: 1,
|
|
units: 'credits',
|
|
placeholder: null,
|
|
}),
|
|
// Max tier knobs.
|
|
max_monthly: Value.union(
|
|
{
|
|
name: 'Max — Monthly Credits',
|
|
description:
|
|
'Max-tier users default to unlimited monthly credits. Switch to "Limit to a specific amount" to cap how many credits each Max install can spend per month.',
|
|
default: 'unlimited',
|
|
},
|
|
maxMonthlyVariants,
|
|
),
|
|
max_gemini_cap: Value.number({
|
|
name: 'Max — Gemini Cap (monthly)',
|
|
description:
|
|
'Within the Max monthly allowance, how many credits may be served via Gemini (the rest spill to the operator-hardware fallback). Default 50.',
|
|
required: true,
|
|
default: 50,
|
|
min: 0,
|
|
max: 1_000_000,
|
|
integer: true,
|
|
step: 1,
|
|
units: 'credits',
|
|
placeholder: null,
|
|
}),
|
|
})
|
|
|
|
export const adjustTierQuotas = sdk.Action.withInput(
|
|
'adjust-tier-quotas',
|
|
|
|
async ({ effects }) => ({
|
|
name: 'Adjust Tier Quotas',
|
|
description:
|
|
'Tune the per-tier monthly credit caps and Gemini exposure without redeploying. Changes apply to the next request — no restart needed.',
|
|
warning: null,
|
|
allowedStatuses: 'any',
|
|
group: null,
|
|
visibility: 'enabled',
|
|
}),
|
|
|
|
inputSpec,
|
|
|
|
async ({ effects }) => {
|
|
const config = await configFile.read().once()
|
|
let parsed: any = {}
|
|
try {
|
|
parsed = JSON.parse(config?.relay_tier_quotas_json || '{}')
|
|
} catch {
|
|
parsed = {}
|
|
}
|
|
// Translate the saved `max.monthly` value back into the union shape.
|
|
// null → unlimited; number → limited with that amount.
|
|
const savedMaxMonthly = parsed?.max?.monthly
|
|
const maxMonthlyUnion =
|
|
savedMaxMonthly == null
|
|
? { selection: 'unlimited' as const, value: {} }
|
|
: {
|
|
selection: 'limited' as const,
|
|
value: { amount: savedMaxMonthly },
|
|
}
|
|
return {
|
|
core_lifetime: parsed?.core?.lifetime ?? 10,
|
|
core_gemini_cap: parsed?.core?.geminiCapLifetime ?? 5,
|
|
pro_monthly: parsed?.pro?.monthly ?? 50,
|
|
pro_gemini_cap: parsed?.pro?.geminiCapMonthly ?? 25,
|
|
max_monthly: maxMonthlyUnion,
|
|
max_gemini_cap: parsed?.max?.geminiCapMonthly ?? 50,
|
|
}
|
|
},
|
|
|
|
async ({ effects, input }) => {
|
|
// Resolve the Max monthly union back to a primitive value for
|
|
// storage. The union arrives as { selection, value }; "unlimited"
|
|
// → null, "limited" → the inner amount.
|
|
const mmSel = (input.max_monthly as any)?.selection
|
|
const mmVal = (input.max_monthly as any)?.value
|
|
const maxMonthly =
|
|
mmSel === 'limited'
|
|
? Number(mmVal?.amount ?? 1000)
|
|
: null
|
|
|
|
const quotas = {
|
|
core: {
|
|
lifetime: input.core_lifetime ?? 10,
|
|
geminiCapLifetime: input.core_gemini_cap ?? 5,
|
|
monthly: null,
|
|
geminiCapMonthly: null,
|
|
},
|
|
pro: {
|
|
lifetime: null,
|
|
monthly: input.pro_monthly ?? 50,
|
|
geminiCapMonthly: input.pro_gemini_cap ?? 25,
|
|
},
|
|
max: {
|
|
lifetime: null,
|
|
monthly: maxMonthly,
|
|
geminiCapMonthly: input.max_gemini_cap ?? 50,
|
|
},
|
|
}
|
|
await configFile.merge(effects, {
|
|
relay_tier_quotas_json: JSON.stringify(quotas),
|
|
})
|
|
return null
|
|
},
|
|
)
|