Pipeline
- CORS preflights send Access-Control-Max-Age: browsers stop re-preflighting authed calls every 5 s.
- `req.ua` is parsed on first read instead of on every request.
- JSON `rawBody` reuses the parsed buffer instead of copying it.
- `emitAndWait` returns early when nothing listens: no span or closure per unlistened driver call.
- SQLBatcher sends a lone query on the next tick while fewer than 8 flushes are in flight, and
waits `maxTimeInQueue` as before otherwise. Lone reads skip the 10/30 ms wait; batching under
load is unchanged.
Auth and permissions
- Token-to-actor auth starts the user, session and app lookups together and checks them in the
same order: fewer sequential Redis round trips per authed request.
- get-user-app-token and check-app reuse the resolved app row. getOrCreateApp skips the re-select
after its own insert and doesn't wait on the cache write for replica rows.
- getOrCreateApp reads the race winner from the primary when its insert loses, so it no longer
hands out a session that was never written.
- whoami runs the email check and the OIDC/taskbar/team lookups in parallel; key order unchanged.
- OIDC providers are memoized once every discovery succeeds, with one discovery fetch in flight
per URL, and the Apple key is parsed once.
- gui-token and sync-cookie use `req.actor.user` instead of re-reading the user.
- listSessions batches app lookups. App icon probes run together, and the original-icon probe
skips the lineage fallback.
- Default-granted permissions are answered from config before the scan cache, and exploded
options are deduped: one less Redis read per /drivers/call.
- ACL resolves ancestors lazily (owners and full-access tokens skip them) and scans the node
first, then the ancestors concurrently on a miss.
Filesystem
- Missing-path lineage lookups run 10 at a time: batch-upload path resolution drops from about
1.3 s to about 40 ms per 100 new files.
- MySQL single-file inserts return the row built from the written values, falling back to the
primary read: one less primary round trip per new file.
- Batch thumbnails are stored with one UPDATE per chunk (200 rows or 4M chars), with a per-row
fallback.
- storage.quota.bonus listeners run concurrently next to the usage SUM, and the payload carries
`userUuid`: quota checks and /df no longer serialize the listener reads.
- readdir cache backfill is one pipeline instead of 3xN SETEX.
- Profile reads skip the lineage fallback; the own profile is read alongside the user lookup.
- Legacy /stat runs is_empty (new `hasChildrenByParentUid`), owner and thumbnail concurrently.
- Downloads and the video proxy pipe through `pipeToResponse`: aborts release the storage stream
at once with no extra 'close' listeners, and the video proxy no longer crashes on a mid-stream
upstream error.
- mkdir looks up target and parent together.
- Upload completion reuses the session from the access check (status is still re-read before
completing), and multipart signing reads it once.
- Moving a shared file to Trash skips two lookups that always returned the entry itself.
Drivers and events
- Subscription resolvers run concurrently and are folded in registration order.
- SubdomainDriver and WorkerDriver overlap independent lookups.
- The events "anyone subscribed?" check is one Redis pipeline when the region is warm.
- Handler publish overlaps the count with the size check.
- Broadcast flushes peers concurrently over a keep-alive agent, so a slow peer no longer delays
cross-region invalidations to the others.
Sites, share and AI
- Hosted sites resolve the owner first, then the app and root together. Site config loads only
on 404s. The hosted-app query selects only the columns it uses and moves into AppStore.
- shareFlags asks about the plan only when an item is shared by link only.
- Share listings run the count, teams and plan reads alongside their page query.
- Claude attachment cleanup no longer blocks the response.
- New util `awaitLater` for promises started early and awaited later.
- Chat driver runs each attempt through one helper and checks the abort
signal only on entry and in the attempt catch blocks, where it keeps a
cancelled request from marking the route unhealthy.
- A stream ended after cancellation still emits ai.prompt.cost-calculated;
only ai.prompt.complete is skipped.
- pipeNdjsonStream owns its line buffer and onEnd is optional; drop the
errored guards that can no longer fire.
- Drop the Claude post-loop abort check (finalMessage already throws).
- Skip provider error logs when the caller cancelled.
- Simplify the usage_limited cap condition.
Narrows the wire-shape changes to the callers that need them.
- request-level upstream 4xx stays strict only on the Anthropic route;
/drivers/call and the OpenAI routes fall back on any error again
- outage chains return 400 with the old codes; the Anthropic error
renderer maps them to 502/529 from the carried upstream status
- native Claude results report finish_reason 'stop' again (real reason
in stopReason); OpenAI routes still map the real value, and a stream
that carried tool calls never ends as a plain stop
- Claude usage carries thinking_tokens again, informational only and
added after pricing and the funds cap
- reasoning_effort is clamped to values each OpenAI model accepts;
Hoonify, MiniMax, ZAI and Meta only receive params they support
- OpenAI-form tool_choice from /drivers/call is normalized again
- /responses accepts background:false; mcp_servers is ignored; legacy
media block shapes pass the Anthropic route's validation
- OpenAI web search holds one call; only search actions are billed;
Azure bills Bing grounding requests; usd_cents includes search fees
The Anthropic-compatible route flattened requests into the OpenAI-style
driver shape and rebuilt them for Claude, which dropped system blocks,
thinking, tool_choice, cache_control, betas and stop reasons. Claude
Code requests 400ed upstream and were silently served by a reseller.
- normalized driver args, stream chunks and results gain tool choice,
thinking, output format, stop sequences, caching, stop reasons, usage
details, reasoning signatures, server-tool blocks and safeguard results
- ClaudeProvider builds requests from those fields with a beta, tool and
body-field allowlist; meters fast mode, web search and advisor calls,
and no longer bills thinking tokens twice
- fallback only on route-level failures; outages map to 502/529
- vendor error envelopes on the Anthropic and OpenAI routes
- Anthropic route: native SSE with pings, count_tokens, /v1/models
- OpenAI-family providers and routes map the same fields; Responses web
search is metered; reasoning_effort gating fixed; /responses rejects
org-scoped stateful params and forces store:false
- puter.js: typedefs, new chat options, silent toString for new chunks
* docs(agents): no credits or bug-report mentions in commits and PRs
* fix(ai): chat credit gate ceiling, in-flight holds and disconnects
- Cap output at min(max_tokens, context - prompt). The gate used
max_tokens - prompt, so any prompt longer than a model's output limit
was refused with 402 regardless of balance. Gemini 2.5 Pro's output
limit corrected to 65,536.
- Abort the upstream stream when the caller disconnects (driver calls and
the OpenAI/Anthropic-compatible routes), which ends the credit hold.
- A request refused only because the account's own in-flight requests
hold its balance gets 429 `credits_reserved` instead of 402; puter.js
retries it with exponential backoff (1s to 30s, about 2 minutes).
- When the balance caps a completion and it uses the whole cap, the
response carries `usage_limited` (non-stream `metadata`, stream `usage`
line), which puter.js already turns into the upgrade prompt.
* perf(ai): read the balance and its holds once per credit gate
The gate re-read holds, and the whole balance when any were held, to tell
spent from committed. getUsageHeadroom returns both halves from the same
reads getRemainingUsage already did.
* fix(ai): make chat fallback reach streamed Claude calls and rank Azure explicitly
- ClaudeProvider opens the upstream stream and awaits its connection
before returning the populator, so an overloaded or rate-limited route
throws from complete() and reaches the driver's fallback loop instead
of surfacing as an error frame on a 200
- the OpenAI-compatible chat and completions routes only pin a provider
when the caller sent one, so they get the same preferred healthy route
puter.js callers do and unhealthy-route skipping applies to their
first attempt
- Azure is ranked ahead of the vendors it fronts by an explicit tier in
modelRouting rather than a price tie plus registration order
- drop Together's synthetic always-failing model-fallback-test-1 entry
- test that a 4xx leaves a route in rotation while a 503 marks it
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(ai): surface swallowed Claude stream errors, keep compat-route defaults
Review follow-ups on the fallback work.
- The pre-created event iterator only receives an error if a reader is
already waiting on it, so a failure landing between the connect and the
populator's first pull ended the stream cleanly — truncated content
billed and reported as a success. Rethrow when the stream is errored.
- A refused stream deleted its Anthropic uploads but left the caller's
message parts pointing at those file ids, so the fallback route was
handed handles it cannot resolve. processPuterPathUploads now returns a
restore() that both failure paths call.
- The OpenAI-compat routes keep pinning OpenAI when the caller sends no
model at all, so the default model stays put instead of moving to
Azure's.
- Say why /openai/v1/responses and /anthropic/v1/messages stay pinned:
each translates one provider's native shape by hand.
- PREFERRED_PROVIDERS is unexported and its doc now states the rank is
unconditional; the duplicated hidden-model list is one constant.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(ai): undo the puter_path rewrite by field instead of snapshotting the part
Copying the content part kept whatever the caller sent on it — a large
inline `source` or `text` alongside `puter_path` — reachable until the
request ended, where overwriting the field used to make it garbage right
away. The only fields this function writes are `type`/`source` on success
and `type`/`text` on failure, and the fallback uploader keys off
`puter_path` alone, so restore undoes those three by name.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Introduces a new `normalize` option for chat completions, plus release-date based default normalization (post-2026-09-01) to coerce provider-native outputs into a consistent OpenAI-style shape. Adds shared normalization utilities, extensive driver/provider consistency tests, and controller safeguards that pin provider-native output where route-specific translators are used. Also wires the option through puter.js (`chat` options and `puter.ai.normalize` default), updates AI/chat response docs and examples, and resolves related TypeScript typing issues reflected in the typecheck baseline.
* feat: add BytePlus ModelArk chat provider
Adds BytePlus ModelArk as a provider for the puter-chat-completion
driver, following the MiniMax/ZAI providers as reference per
doc/contributing-apis.md.
- OpenAI-compatible endpoint at ark.ap-southeast.bytepluses.com/api/v3
(apiBaseUrl config selects the region)
- Static catalog of 16 chat models (Seed 2.x/1.x incl. vision, GLM,
DeepSeek, GPT-OSS) with limits and per-token pricing from the
official docs
- Passes Ark's thinking/response_format/stop params through custom;
normalizes reasoning_content to reasoning
- Bare deepseek-v4-* names stay with the first-party DeepSeek
provider; BytePlus only claims prefixed aliases
- Offline unit tests (mocked SDK against a real test server) plus an
env-gated integration test
* feat: add BytePlus image and video providers
Extends the BytePlus ModelArk integration to the puter-image-generation
and puter-video-generation drivers, reusing the same services.byteplus
API key and regional apiBaseUrl as the chat provider.
Image (Seedream/SeedEdit via OpenAI-compatible /images/generations):
- dola-seedream-5-0-pro (pixel-tier pricing + billed input images from
the 2nd on), seedream-5-0-lite, 4-5, 4-0, and seededit-3-0-i2i
- quality tiers 1K/1.5K/2K; aspect ratios resolve to Ark's documented
pixel sizes; explicit WxH passes through with Ark's bounds enforced
Video (Seedance via Ark's async /contents/generations/tasks + polling):
- Seedance 2.0 / 2.0 Fast / 2.0 Mini / 1.5 Pro / 1.0 Pro / 1.0 Pro Fast
(2.5 is priced but its API isn't live yet, so it's excluded)
- per-video-token billing from usage.completion_tokens, with per-second
estimates feeding the credit cap; audio vs silent rates for 1.5 Pro
- first/last frame and reference-image inputs; generate_audio param
added to IGenerateVideoParams
Pricing and capabilities hardcoded from the official docs (ModelArk
pages 1544106, 1330310, 1520757, 1521309, 1541523). Offline unit tests
mock the SDK / global fetch; integration tests are env-gated on
PUTER_TEST_AI_BYTEPLUS_API_KEY.
* fix: correct BytePlus catalogs and validation against the live API
Verified the three BytePlus providers against ModelArk with a real key;
these are the mismatches that surfaced.
- Drop seededit-3-0-i2i-250628. Ark reports it as Shutdown and every
request 404s. Its now-unreachable image-to-image branches in the
provider go with it.
- seedream-4-5 and the 5.0 series enforce a 3,686,400 pixel minimum, so
they only accept the 2K tier. Mark them 2k-only and snap an
unsupported tier up to the nearest allowed one, which also keeps the
aspect-ratio table from mapping to a sub-minimum size.
- glm-4-7 has a 204,800 token context, not 256K.
- Guard the actor in the image provider like the video provider does.
- Round a sub-minimum video duration up to the shortest supported clip
instead of reporting it as insufficient funds.
- Gate video resolution on the model's own dimensions; the dims table is
shared across a family and accepts more than any one model does.
* Tighten BytePlus AI provider handling
Extract shared reasoning-content normalization for OpenAI-style chat providers, and harden BytePlus image/video behavior. This updates image tier and size validation, normalizes aspect ratios and input image refs, prevents mismatched BytePlus key/base URL fallback config, makes video resolution matching case-insensitive, and rejects excess reference images instead of silently truncating them. Tests were expanded to cover the new BytePlus request and validation paths.
- declare rate + concurrency limits on every route and driver that lacked one
- add acquireConcurrent for websocket connections and the DAV mount
- bucket AI models by identity key only; keep resold duplicates of any vendor
- skip recently-failed provider routes; cap the fallback chain at 3 attempts
- let full-access access tokens bind a worker to an app their own user owns
- cache resolved subscriptions so tiered limits don't add a round trip
* Alpha: compaction support for OpenAI and Anthropic
* update lock
* fix billing for anthropic compactions
* Fix max_tokens bug in together provider
* Fix responses compaction
* tests: add unit tests for PuterAIController: [#2965]
Adds offline PuterAIController.test.ts with stubbed drivers covering:
route registration (paths and per-route auth options), app-actor
gating across all four chat-proxy routes, body validation (messages
must be an array; openaiCompletions prompt must be a string;
openaiResponses provider must be openai-responses), driver delegation
and request shaping (model + messages + provider defaults), response
shaping for OpenAI chat-completion / text-completion / Responses
envelopes and Anthropic message envelope (text + tool_use blocks),
SSE streaming with [DONE] terminator, model-listing endpoints with
hidden-id filtering and 501 when list() is unavailable, and the
HMAC-gated video proxy's input/expiry/secret guards.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* tests: PuterAIController uses setupTestServer + spy on aiChat.complete
Drops the manual driver/controller stub apparatus (makeStubDriver,
makeController, custom dep builders) in favour of the live wired
controller from server.controllers.puterAi. Each test that needs a
canned chat completion result spies on server.drivers.aiChat.complete,
which keeps the unit-of-test focused on the controller (validation,
response shape, SSE streaming) without dragging in chat-driver model
resolution and credit checks. Aligns with AGENTS.md: "Prefer test
server over mocking deps."
The "500 when URL signature secret missing" assertion was dropped —
the default config wires a secret, so the branch is unreachable in
the live test server. Replaced with the equivalent 403-on-invalid-
signature assertion which actually exercises the HMAC gate.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>