Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .changelog/NEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
12 changes: 11 additions & 1 deletion client/src/pages/AIProviders.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -467,6 +467,16 @@ export default function AIProviders() {
)}
</div>

{isClaudeCodePlanCli(provider) && (
<div className="mt-2 text-xs rounded-md border border-port-warning/40 bg-port-warning/10 text-port-warning px-2.5 py-2 leading-relaxed">
⚠️ Starting <span className="font-semibold">June 15, 2026</span>, Anthropic clocks
this headless Claude Code usage under <span className="font-semibold">API billing</span> —
it will consume extra API credits instead of your Claude Code plan. Avoid this
provider; use the interactive <span className="font-semibold">Claude Code TUI</span> provider,
which stays on the plan.
</div>
)}

{testResults[provider.id] && !testResults[provider.id].testing && (
<div className={`mt-2 text-sm ${testResults[provider.id].success ? 'text-port-success' : 'text-port-error'}`}>
{testResults[provider.id].success
Expand Down
23 changes: 23 additions & 0 deletions client/src/utils/providers.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Comment thread
atomantic marked this conversation as resolved.

/**
* Resolve the provider whose timeout is the "fallback" for a stage — the
* stage's pinned provider when set, otherwise the active provider. Used to
Expand Down
27 changes: 27 additions & 0 deletions client/src/utils/providers.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
isCliProvider,
isApiProvider,
isProcessProvider,
isClaudeCodePlanCli,
enabledApiProviderFilter,
providerTypeClass,
getProviderTimeout,
Expand Down Expand Up @@ -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 = [
Expand Down