fix(coding-agent): warn on Anthropic subscription auth
This commit is contained in:
@@ -6,7 +6,7 @@
|
||||
|
||||
- Fixed bare `readline` import to use `node:readline` prefix for Deno compatibility ([#2885](https://github.com/badlogic/pi-mono/issues/2885) by [@milosv-vtool](https://github.com/milosv-vtool))
|
||||
- Fixed auto-retry to treat stream failures like `request ended without sending any chunks` as transient errors ([#2892](https://github.com/badlogic/pi-mono/issues/2892))
|
||||
- Fixed interactive startup notices to render after the initial resource listing, and added a bundled Earendil startup announcement with inline image rendering for April 8, 2026.
|
||||
- Fixed interactive startup notices to render after the initial resource listing, and added a bundled Earendil startup announcement with inline image rendering for April 8 and 9, 2026. Moved the blog link above the image to avoid overlap with terminal image rendering.
|
||||
|
||||
## [0.65.2] - 2026-04-06
|
||||
|
||||
|
||||
@@ -31,6 +31,9 @@ export class EarendilAnnouncementComponent extends Container {
|
||||
this.addChild(new DynamicBorder((text) => theme.fg("accent", text)));
|
||||
this.addChild(new Text(theme.bold(theme.fg("accent", "pi has joined Earendil")), 1, 0));
|
||||
this.addChild(new Spacer(1));
|
||||
this.addChild(new Text(theme.fg("muted", "Read the blog post:"), 1, 0));
|
||||
this.addChild(new Text(theme.fg("mdLink", BLOG_URL), 1, 0));
|
||||
this.addChild(new Spacer(1));
|
||||
|
||||
const imageBase64 = loadImageBase64();
|
||||
if (imageBase64) {
|
||||
@@ -45,8 +48,6 @@ export class EarendilAnnouncementComponent extends Container {
|
||||
this.addChild(new Spacer(1));
|
||||
}
|
||||
|
||||
this.addChild(new Text(theme.fg("muted", "Read the blog post:"), 1, 0));
|
||||
this.addChild(new Text(theme.fg("mdLink", BLOG_URL), 1, 0));
|
||||
this.addChild(new DynamicBorder((text) => theme.fg("accent", text)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,6 +129,13 @@ type CompactionQueuedMessage = {
|
||||
mode: "steer" | "followUp";
|
||||
};
|
||||
|
||||
const ANTHROPIC_SUBSCRIPTION_AUTH_WARNING =
|
||||
"Anthropic subscription auth is active. Third-party usage now draws from extra usage and is billed per token, not your Claude plan limits. Manage extra usage at https://claude.ai/settings/usage.";
|
||||
|
||||
function isAnthropicSubscriptionAuthKey(apiKey: string | undefined): boolean {
|
||||
return typeof apiKey === "string" && apiKey.startsWith("sk-ant-oat");
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for InteractiveMode initialization.
|
||||
*/
|
||||
@@ -175,6 +182,7 @@ export class InteractiveMode {
|
||||
private lastEscapeTime = 0;
|
||||
private changelogMarkdown: string | undefined = undefined;
|
||||
private startupNoticesShown = false;
|
||||
private anthropicSubscriptionWarningShown = false;
|
||||
|
||||
// Status line tracking (for mutating immediately-sequential status updates)
|
||||
private lastStatusSpacer: Spacer | undefined = undefined;
|
||||
@@ -431,7 +439,7 @@ export class InteractiveMode {
|
||||
|
||||
private shouldShowEarendilAnnouncement(): boolean {
|
||||
const now = new Date();
|
||||
return now.getFullYear() === 2026 && now.getMonth() === 3 && now.getDate() === 8;
|
||||
return now.getFullYear() === 2026 && now.getMonth() === 3 && (now.getDate() === 8 || now.getDate() === 9);
|
||||
}
|
||||
|
||||
private showStartupNoticesIfNeeded(): void {
|
||||
@@ -631,6 +639,8 @@ export class InteractiveMode {
|
||||
this.showWarning(modelFallbackMessage);
|
||||
}
|
||||
|
||||
void this.maybeWarnAboutAnthropicSubscriptionAuth();
|
||||
|
||||
// Process initial messages
|
||||
if (initialMessage) {
|
||||
try {
|
||||
@@ -2980,6 +2990,7 @@ export class InteractiveMode {
|
||||
const thinkingStr =
|
||||
result.model.reasoning && result.thinkingLevel !== "off" ? ` (thinking: ${result.thinkingLevel})` : "";
|
||||
this.showStatus(`Switched to ${result.model.name || result.model.id}${thinkingStr}`);
|
||||
void this.maybeWarnAboutAnthropicSubscriptionAuth(result.model);
|
||||
}
|
||||
} catch (error) {
|
||||
this.showError(error instanceof Error ? error.message : String(error));
|
||||
@@ -3476,6 +3487,7 @@ export class InteractiveMode {
|
||||
this.footer.invalidate();
|
||||
this.updateEditorBorderColor();
|
||||
this.showStatus(`Model: ${model.id}`);
|
||||
void this.maybeWarnAboutAnthropicSubscriptionAuth(model);
|
||||
this.checkDaxnutsEasterEgg(model);
|
||||
} catch (error) {
|
||||
this.showError(error instanceof Error ? error.message : String(error));
|
||||
@@ -3511,6 +3523,35 @@ export class InteractiveMode {
|
||||
this.footerDataProvider.setAvailableProviderCount(uniqueProviders.size);
|
||||
}
|
||||
|
||||
private async maybeWarnAboutAnthropicSubscriptionAuth(
|
||||
model: Model<any> | undefined = this.session.model,
|
||||
): Promise<void> {
|
||||
if (this.anthropicSubscriptionWarningShown) {
|
||||
return;
|
||||
}
|
||||
if (!model || model.provider !== "anthropic") {
|
||||
return;
|
||||
}
|
||||
|
||||
const storedCredential = this.session.modelRegistry.authStorage.get("anthropic");
|
||||
if (storedCredential?.type === "oauth") {
|
||||
this.anthropicSubscriptionWarningShown = true;
|
||||
this.showWarning(ANTHROPIC_SUBSCRIPTION_AUTH_WARNING);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const apiKey = await this.session.modelRegistry.getApiKeyForProvider(model.provider);
|
||||
if (!isAnthropicSubscriptionAuthKey(apiKey)) {
|
||||
return;
|
||||
}
|
||||
this.anthropicSubscriptionWarningShown = true;
|
||||
this.showWarning(ANTHROPIC_SUBSCRIPTION_AUTH_WARNING);
|
||||
} catch {
|
||||
// Ignore auth lookup failures for warning-only checks.
|
||||
}
|
||||
}
|
||||
|
||||
private showModelSelector(initialSearchInput?: string): void {
|
||||
this.showSelector((done) => {
|
||||
const selector = new ModelSelectorComponent(
|
||||
@@ -3526,6 +3567,7 @@ export class InteractiveMode {
|
||||
this.updateEditorBorderColor();
|
||||
done();
|
||||
this.showStatus(`Model: ${model.id}`);
|
||||
void this.maybeWarnAboutAnthropicSubscriptionAuth(model);
|
||||
this.checkDaxnutsEasterEgg(model);
|
||||
} catch (error) {
|
||||
done();
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
import { InteractiveMode } from "../src/modes/interactive/interactive-mode.js";
|
||||
|
||||
describe("InteractiveMode.maybeWarnAboutAnthropicSubscriptionAuth", () => {
|
||||
test("warns once when Anthropic subscription auth is detected", async () => {
|
||||
const fakeThis: any = {
|
||||
anthropicSubscriptionWarningShown: false,
|
||||
session: {
|
||||
modelRegistry: {
|
||||
authStorage: {
|
||||
get: vi.fn().mockReturnValue(undefined),
|
||||
},
|
||||
getApiKeyForProvider: vi.fn().mockResolvedValue("sk-ant-oat01-test"),
|
||||
},
|
||||
},
|
||||
showWarning: vi.fn(),
|
||||
};
|
||||
|
||||
await (InteractiveMode as any).prototype.maybeWarnAboutAnthropicSubscriptionAuth.call(fakeThis, {
|
||||
provider: "anthropic",
|
||||
});
|
||||
await (InteractiveMode as any).prototype.maybeWarnAboutAnthropicSubscriptionAuth.call(fakeThis, {
|
||||
provider: "anthropic",
|
||||
});
|
||||
|
||||
expect(fakeThis.showWarning).toHaveBeenCalledTimes(1);
|
||||
expect(fakeThis.session.modelRegistry.getApiKeyForProvider).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test("warns when Anthropic OAuth is stored even if token refresh lookup would fail", async () => {
|
||||
const fakeThis: any = {
|
||||
anthropicSubscriptionWarningShown: false,
|
||||
session: {
|
||||
modelRegistry: {
|
||||
authStorage: {
|
||||
get: vi.fn().mockReturnValue({ type: "oauth" }),
|
||||
},
|
||||
getApiKeyForProvider: vi.fn().mockResolvedValue(undefined),
|
||||
},
|
||||
},
|
||||
showWarning: vi.fn(),
|
||||
};
|
||||
|
||||
await (InteractiveMode as any).prototype.maybeWarnAboutAnthropicSubscriptionAuth.call(fakeThis, {
|
||||
provider: "anthropic",
|
||||
});
|
||||
|
||||
expect(fakeThis.showWarning).toHaveBeenCalledTimes(1);
|
||||
expect(fakeThis.session.modelRegistry.getApiKeyForProvider).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("does not warn for non-Anthropic models", async () => {
|
||||
const fakeThis: any = {
|
||||
anthropicSubscriptionWarningShown: false,
|
||||
session: {
|
||||
modelRegistry: {
|
||||
authStorage: {
|
||||
get: vi.fn(),
|
||||
},
|
||||
getApiKeyForProvider: vi.fn(),
|
||||
},
|
||||
},
|
||||
showWarning: vi.fn(),
|
||||
};
|
||||
|
||||
await (InteractiveMode as any).prototype.maybeWarnAboutAnthropicSubscriptionAuth.call(fakeThis, {
|
||||
provider: "openai",
|
||||
});
|
||||
|
||||
expect(fakeThis.showWarning).not.toHaveBeenCalled();
|
||||
expect(fakeThis.session.modelRegistry.getApiKeyForProvider).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user