Fix pi/opencode billing mode ternary bug and add UI field
- Fix: pi-local and opencode-local now correctly return "api" when billingMode is "metered"/"api" instead of falling through to "unknown" - Add tests covering the pi/opencode unknown-default pattern - Add BillingModeField dropdown to the agent config form for all local adapters (Auto / Subscription / Metered API) - Add billingMode to CreateConfigValues type Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
19dc1c6d1d
commit
a222addb8c
7 changed files with 78 additions and 2 deletions
|
|
@ -344,6 +344,7 @@ export interface CreateConfigValues {
|
||||||
workspaceBranchTemplate?: string;
|
workspaceBranchTemplate?: string;
|
||||||
worktreeParentDir?: string;
|
worktreeParentDir?: string;
|
||||||
runtimeServicesJson?: string;
|
runtimeServicesJson?: string;
|
||||||
|
billingMode?: string;
|
||||||
maxTurnsPerRun: number;
|
maxTurnsPerRun: number;
|
||||||
heartbeatEnabled: boolean;
|
heartbeatEnabled: boolean;
|
||||||
intervalSec: number;
|
intervalSec: number;
|
||||||
|
|
|
||||||
|
|
@ -372,7 +372,10 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
||||||
provider: parseModelProvider(modelId),
|
provider: parseModelProvider(modelId),
|
||||||
biller: resolveOpenCodeBiller(runtimeEnv, parseModelProvider(modelId)),
|
biller: resolveOpenCodeBiller(runtimeEnv, parseModelProvider(modelId)),
|
||||||
model: modelId,
|
model: modelId,
|
||||||
billingType: applyBillingModeOverride("api", asString(config.billingMode, "auto")) === "subscription" ? "subscription" : "unknown",
|
billingType: (() => {
|
||||||
|
const mode = asString(config.billingMode, "auto").trim().toLowerCase();
|
||||||
|
return mode === "auto" || mode === "" ? "unknown" : applyBillingModeOverride("api", mode);
|
||||||
|
})(),
|
||||||
costUsd: attempt.parsed.costUsd,
|
costUsd: attempt.parsed.costUsd,
|
||||||
resultJson: {
|
resultJson: {
|
||||||
stdout: attempt.proc.stdout,
|
stdout: attempt.proc.stdout,
|
||||||
|
|
|
||||||
|
|
@ -461,7 +461,10 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
||||||
provider: provider,
|
provider: provider,
|
||||||
biller: resolvePiBiller(runtimeEnv, provider),
|
biller: resolvePiBiller(runtimeEnv, provider),
|
||||||
model: model,
|
model: model,
|
||||||
billingType: applyBillingModeOverride("api", asString(config.billingMode, "auto")) === "subscription" ? "subscription" : "unknown",
|
billingType: (() => {
|
||||||
|
const mode = asString(config.billingMode, "auto").trim().toLowerCase();
|
||||||
|
return mode === "auto" || mode === "" ? "unknown" : applyBillingModeOverride("api", mode);
|
||||||
|
})(),
|
||||||
costUsd: attempt.parsed.usage.costUsd,
|
costUsd: attempt.parsed.usage.costUsd,
|
||||||
resultJson: {
|
resultJson: {
|
||||||
stdout: attempt.proc.stdout,
|
stdout: attempt.proc.stdout,
|
||||||
|
|
|
||||||
|
|
@ -37,3 +37,32 @@ describe("applyBillingModeOverride", () => {
|
||||||
expect(applyBillingModeOverride("subscription", "credits")).toBe("subscription");
|
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");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
||||||
36
ui/src/adapters/billing-mode-field.tsx
Normal file
36
ui/src/adapters/billing-mode-field.tsx
Normal file
|
|
@ -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<AdapterConfigFieldsProps, "isCreate" | "values" | "set" | "config" | "eff" | "mark">) {
|
||||||
|
const value = isCreate
|
||||||
|
? String(values?.billingMode ?? "auto")
|
||||||
|
: eff("adapterConfig", "billingMode", String(config.billingMode ?? "auto"));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Field label="Billing mode" hint={help.billingMode}>
|
||||||
|
<select
|
||||||
|
value={value}
|
||||||
|
onChange={(e) =>
|
||||||
|
isCreate
|
||||||
|
? set?.({ billingMode: e.target.value })
|
||||||
|
: mark("adapterConfig", "billingMode", e.target.value === "auto" ? undefined : e.target.value)
|
||||||
|
}
|
||||||
|
className={selectClass}
|
||||||
|
>
|
||||||
|
<option value="auto">Auto (detect from API keys)</option>
|
||||||
|
<option value="subscription">Subscription (non-billable)</option>
|
||||||
|
<option value="metered">Metered API (billable)</option>
|
||||||
|
</select>
|
||||||
|
</Field>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -41,6 +41,7 @@ import {
|
||||||
import { defaultCreateValues } from "./agent-config-defaults";
|
import { defaultCreateValues } from "./agent-config-defaults";
|
||||||
import { getUIAdapter } from "../adapters";
|
import { getUIAdapter } from "../adapters";
|
||||||
import { ClaudeLocalAdvancedFields } from "../adapters/claude-local/config-fields";
|
import { ClaudeLocalAdvancedFields } from "../adapters/claude-local/config-fields";
|
||||||
|
import { BillingModeField } from "../adapters/billing-mode-field";
|
||||||
import { MarkdownEditor } from "./MarkdownEditor";
|
import { MarkdownEditor } from "./MarkdownEditor";
|
||||||
import { ChoosePathButton } from "./PathInstructionsModal";
|
import { ChoosePathButton } from "./PathInstructionsModal";
|
||||||
import { OpenCodeLogoIcon } from "./OpenCodeLogoIcon";
|
import { OpenCodeLogoIcon } from "./OpenCodeLogoIcon";
|
||||||
|
|
@ -773,6 +774,8 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
|
||||||
<ClaudeLocalAdvancedFields {...adapterFieldProps} />
|
<ClaudeLocalAdvancedFields {...adapterFieldProps} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<BillingModeField {...adapterFieldProps} />
|
||||||
|
|
||||||
<Field label="Extra args (comma-separated)" hint={help.extraArgs}>
|
<Field label="Extra args (comma-separated)" hint={help.extraArgs}>
|
||||||
<DraftInput
|
<DraftInput
|
||||||
value={
|
value={
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,7 @@ export const help: Record<string, string> = {
|
||||||
cooldownSec: "Minimum seconds between consecutive heartbeat runs.",
|
cooldownSec: "Minimum seconds between consecutive heartbeat runs.",
|
||||||
maxConcurrentRuns: "Maximum number of heartbeat runs that can execute simultaneously for this agent.",
|
maxConcurrentRuns: "Maximum number of heartbeat runs that can execute simultaneously for this agent.",
|
||||||
budgetMonthlyCents: "Monthly spending limit in cents. 0 means no limit.",
|
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<string, string> = {
|
export const adapterLabels: Record<string, string> = {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue