diff --git a/packages/adapter-utils/src/types.ts b/packages/adapter-utils/src/types.ts index ce89e0e8..79171cbd 100644 --- a/packages/adapter-utils/src/types.ts +++ b/packages/adapter-utils/src/types.ts @@ -344,6 +344,7 @@ export interface CreateConfigValues { workspaceBranchTemplate?: string; worktreeParentDir?: string; runtimeServicesJson?: string; + billingMode?: string; maxTurnsPerRun: number; heartbeatEnabled: boolean; intervalSec: number; diff --git a/packages/adapters/opencode-local/src/server/execute.ts b/packages/adapters/opencode-local/src/server/execute.ts index 94d7f967..330da070 100644 --- a/packages/adapters/opencode-local/src/server/execute.ts +++ b/packages/adapters/opencode-local/src/server/execute.ts @@ -372,7 +372,10 @@ export async function execute(ctx: AdapterExecutionContext): Promise { + const mode = asString(config.billingMode, "auto").trim().toLowerCase(); + return mode === "auto" || mode === "" ? "unknown" : applyBillingModeOverride("api", mode); + })(), costUsd: attempt.parsed.costUsd, resultJson: { stdout: attempt.proc.stdout, diff --git a/packages/adapters/pi-local/src/server/execute.ts b/packages/adapters/pi-local/src/server/execute.ts index 6dc50219..296d6560 100644 --- a/packages/adapters/pi-local/src/server/execute.ts +++ b/packages/adapters/pi-local/src/server/execute.ts @@ -461,7 +461,10 @@ export async function execute(ctx: AdapterExecutionContext): Promise { + const mode = asString(config.billingMode, "auto").trim().toLowerCase(); + return mode === "auto" || mode === "" ? "unknown" : applyBillingModeOverride("api", mode); + })(), costUsd: attempt.parsed.usage.costUsd, resultJson: { stdout: attempt.proc.stdout, diff --git a/server/src/__tests__/billing-mode-override.test.ts b/server/src/__tests__/billing-mode-override.test.ts index 571ddd7a..bbf2af91 100644 --- a/server/src/__tests__/billing-mode-override.test.ts +++ b/server/src/__tests__/billing-mode-override.test.ts @@ -37,3 +37,32 @@ describe("applyBillingModeOverride", () => { expect(applyBillingModeOverride("subscription", "credits")).toBe("subscription"); }); }); + +describe("pi/opencode billing mode pattern (unknown-default adapters)", () => { + // Mirrors the inline pattern used in pi-local and opencode-local where + // the default billing type is "unknown" (no auto-detection available). + function resolveUnknownDefault(billingMode: string): string { + const mode = billingMode.trim().toLowerCase(); + return mode === "auto" || mode === "" ? "unknown" : applyBillingModeOverride("api", mode); + } + + it("returns 'unknown' when billingMode is 'auto' (default)", () => { + expect(resolveUnknownDefault("auto")).toBe("unknown"); + }); + + it("returns 'unknown' when billingMode is empty", () => { + expect(resolveUnknownDefault("")).toBe("unknown"); + }); + + it("returns 'subscription' when billingMode is 'subscription'", () => { + expect(resolveUnknownDefault("subscription")).toBe("subscription"); + }); + + it("returns 'api' when billingMode is 'metered'", () => { + expect(resolveUnknownDefault("metered")).toBe("api"); + }); + + it("returns 'api' when billingMode is 'api'", () => { + expect(resolveUnknownDefault("api")).toBe("api"); + }); +}); diff --git a/ui/src/adapters/billing-mode-field.tsx b/ui/src/adapters/billing-mode-field.tsx new file mode 100644 index 00000000..f72f328d --- /dev/null +++ b/ui/src/adapters/billing-mode-field.tsx @@ -0,0 +1,36 @@ +import type { AdapterConfigFieldsProps } from "./types"; +import { Field, help } from "../components/agent-config-primitives"; + +const selectClass = + "w-full rounded-md border border-border px-2.5 py-1.5 bg-transparent outline-none text-sm font-mono"; + +export function BillingModeField({ + isCreate, + values, + set, + config, + eff, + mark, +}: Pick) { + const value = isCreate + ? String(values?.billingMode ?? "auto") + : eff("adapterConfig", "billingMode", String(config.billingMode ?? "auto")); + + return ( + + + + ); +} diff --git a/ui/src/components/AgentConfigForm.tsx b/ui/src/components/AgentConfigForm.tsx index 1810e9a8..f5eb8f2c 100644 --- a/ui/src/components/AgentConfigForm.tsx +++ b/ui/src/components/AgentConfigForm.tsx @@ -41,6 +41,7 @@ import { import { defaultCreateValues } from "./agent-config-defaults"; import { getUIAdapter } from "../adapters"; import { ClaudeLocalAdvancedFields } from "../adapters/claude-local/config-fields"; +import { BillingModeField } from "../adapters/billing-mode-field"; import { MarkdownEditor } from "./MarkdownEditor"; import { ChoosePathButton } from "./PathInstructionsModal"; import { OpenCodeLogoIcon } from "./OpenCodeLogoIcon"; @@ -773,6 +774,8 @@ export function AgentConfigForm(props: AgentConfigFormProps) { )} + + = { cooldownSec: "Minimum seconds between consecutive heartbeat runs.", maxConcurrentRuns: "Maximum number of heartbeat runs that can execute simultaneously for this agent.", budgetMonthlyCents: "Monthly spending limit in cents. 0 means no limit.", + billingMode: "Controls how runs are billed. Auto detects from API keys. Set to Subscription if you're on a plan (e.g. Claude Max, GPT Plus) so runs don't count toward spend.", }; export const adapterLabels: Record = {