diff --git a/.changelog/NEXT.md b/.changelog/NEXT.md
index 5c2c295abd..eceb04ce92 100644
--- a/.changelog/NEXT.md
+++ b/.changelog/NEXT.md
@@ -27,6 +27,8 @@ TBD
- **Writers Room auto-collapses the header + library when you open a work.** Tapping a library item now collapses both the page header and the library so the editor gets the full screen — most useful on mobile, where the library is an inline block stacked above the editor. The header shrinks to a slim bar that hosts the "show library" control (reachable on mobile, where there was previously no expand affordance), and the library always re-appears whenever no work is selected so you're never stranded.
+- **AI Providers: billing-change warning on the headless Claude Code CLI provider.** The AI Providers page now shows an inline warning on the **Claude Code CLI** (`claude --print`) provider card: starting **June 15, 2026**, Anthropic clocks this non-interactive usage under API billing — consuming extra API credits instead of your Claude Code plan — so it should be avoided in favor of the interactive **Claude Code TUI** provider, which stays on the plan. The warning is scoped to plan-billed `claude` CLI providers and intentionally excludes Bedrock/Vertex-routed variants (those bill via the cloud account, not the plan).
+
## Fixed
- **Ollama model installs no longer die on a transient "EOF".** Pulling a model from the Local LLMs page could fail with a bare `EOF` status and leave the model uninstalled. Ollama streams pull progress as NDJSON, and a transient network hiccup between Ollama and the model registry/CDN surfaces mid-stream as an `{"error":"EOF"}` frame (or the response read dropping outright). PortOS gave up on the first occurrence — even though the `ollama` CLI silently retries these and the pull is resumable (partial blobs are kept). `pullModel` now retries the transient/EOF class up to 3 attempts with a linear backoff, continuing from the partial download rather than restarting; non-transient errors (bad model name, missing manifest) still fail fast. The install banner shows a "retrying after network error" notice during the backoff instead of stalling.
diff --git a/client/src/pages/AIProviders.jsx b/client/src/pages/AIProviders.jsx
index 71439dffb1..1623ad4f9b 100644
--- a/client/src/pages/AIProviders.jsx
+++ b/client/src/pages/AIProviders.jsx
@@ -2,7 +2,7 @@ import { useState, useEffect, useCallback } from 'react';
import toast from '../components/ui/Toast';
import * as api from '../services/api';
import socket from '../services/socket';
-import { filterSelectableModels, providerTypeClass, isTuiProvider, isApiProvider, isProcessProvider } from '../utils/providers';
+import { filterSelectableModels, providerTypeClass, isTuiProvider, isApiProvider, isProcessProvider, isClaudeCodePlanCli } from '../utils/providers';
import {
formatDurationMs,
parseTimeoutMs,
@@ -467,6 +467,16 @@ export default function AIProviders() {
)}
+ {isClaudeCodePlanCli(provider) && (
+
+ ⚠️ Starting June 15, 2026, Anthropic clocks
+ this headless Claude Code usage under API billing —
+ it will consume extra API credits instead of your Claude Code plan. Avoid this
+ provider; use the interactive Claude Code TUI provider,
+ which stays on the plan.
+
+ )}
+
{testResults[provider.id] && !testResults[provider.id].testing && (
{testResults[provider.id].success
diff --git a/client/src/utils/providers.js b/client/src/utils/providers.js
index 80c4d35e55..e103ff2406 100644
--- a/client/src/utils/providers.js
+++ b/client/src/utils/providers.js
@@ -59,6 +59,29 @@ export const enabledApiProviderFilter = (provider) => Boolean(provider?.enabled)
*/
export const isProcessProvider = (provider) => isCliProvider(provider) || isTuiProvider(provider);
+/**
+ * Check if a provider is the headless Claude Code CLI (`claude --print`) whose
+ * *provider-level config* points it at a Claude Code subscription plan — i.e. the
+ * provider's own `envVars` do NOT route it through Bedrock or Vertex (those bill
+ * via the cloud account, not the plan). Used to surface the billing-change warning:
+ * starting 2026-06-15 Anthropic clocks this non-interactive usage under API billing
+ * (consuming API credits) instead of the Claude Code plan, so it should be avoided
+ * in favor of the interactive Claude Code TUI provider.
+ *
+ * Contract is intentionally provider-level only: this is a client-side heuristic
+ * that sees just the saved provider record. A user who routes the spawn to
+ * Bedrock/Vertex *globally* via `~/.claude/settings.json` (merged below
+ * `provider.envVars` in `server/services/agentCliSpawning.js`) rather than on the
+ * provider would be cloud-billed but still match here. Configure Bedrock/Vertex on
+ * the provider's `envVars` (as the shipped `claude-code-bedrock` sample does) to
+ * suppress the warning.
+ */
+export const isClaudeCodePlanCli = (provider) =>
+ isCliProvider(provider) &&
+ provider?.command === 'claude' &&
+ !provider?.envVars?.CLAUDE_CODE_USE_BEDROCK &&
+ !provider?.envVars?.CLAUDE_CODE_USE_VERTEX;
+
/**
* Resolve the provider whose timeout is the "fallback" for a stage — the
* stage's pinned provider when set, otherwise the active provider. Used to
diff --git a/client/src/utils/providers.test.js b/client/src/utils/providers.test.js
index ea90397694..35f1561944 100644
--- a/client/src/utils/providers.test.js
+++ b/client/src/utils/providers.test.js
@@ -7,6 +7,7 @@ import {
isCliProvider,
isApiProvider,
isProcessProvider,
+ isClaudeCodePlanCli,
enabledApiProviderFilter,
providerTypeClass,
getProviderTimeout,
@@ -85,6 +86,32 @@ describe('provider type predicates', () => {
});
});
+describe('isClaudeCodePlanCli', () => {
+ it('matches a headless claude CLI provider on the plan', () => {
+ expect(isClaudeCodePlanCli({ type: 'cli', command: 'claude', envVars: {} })).toBe(true);
+ expect(isClaudeCodePlanCli({ type: 'cli', command: 'claude' })).toBe(true);
+ });
+
+ it('does not match the interactive Claude Code TUI provider', () => {
+ expect(isClaudeCodePlanCli({ type: 'tui', command: 'claude' })).toBe(false);
+ });
+
+ it('does not match non-claude CLI providers', () => {
+ expect(isClaudeCodePlanCli({ type: 'cli', command: 'gemini' })).toBe(false);
+ expect(isClaudeCodePlanCli({ type: 'cli', command: 'codex' })).toBe(false);
+ });
+
+ it('excludes Bedrock/Vertex-routed claude CLIs (billed via cloud, not the plan)', () => {
+ expect(isClaudeCodePlanCli({ type: 'cli', command: 'claude', envVars: { CLAUDE_CODE_USE_BEDROCK: '1' } })).toBe(false);
+ expect(isClaudeCodePlanCli({ type: 'cli', command: 'claude', envVars: { CLAUDE_CODE_USE_VERTEX: '1' } })).toBe(false);
+ });
+
+ it('safely returns false for nullish input', () => {
+ expect(isClaudeCodePlanCli(null)).toBe(false);
+ expect(isClaudeCodePlanCli(undefined)).toBe(false);
+ });
+});
+
describe('enabledApiProviderFilter', () => {
it('keeps only enabled api providers', () => {
const list = [