237 Commits
Author SHA1 Message Date
Daniel Salazar a8a78736bb feat: kv list ordering toggle + addressable path improvements (#3843)
Maintain Release Merge PR / update-release-pr (push) Canceled after 0s
Notify HeyPuter / notify (push) Canceled after 0s
release-please / release-please (push) Canceled after 0s
* fix: put-1787

* feat: kv list reverse
2026-09-09 17:24:03 -07:00
404oopsandClaude Fable 5.1 6146ab9dd3 feat(ai): refresh txt2vid catalogs, retire Sora, default to Veo 3.1 Lite (#3828)
OpenAI shuts the Sora Videos API down on 2026-09-24 and sora-2 was the
default txt2vid model, so the default moves to Veo 3.1 Lite on Gemini
and the OpenAI video provider goes. While there, the video catalogs are
brought in line with what each vendor serves today, the request options
are unified across providers, and the txt2vid docs are rewritten.

- driver: default provider gemini-video-generation with
  veo-3.1-lite-generate-preview; a request under the generic `ai-video`
  driver name lands on the default instead of the first-registered
  provider; `WIDTHxHEIGHT` sizes map onto tier catalogs by the shorter
  side and fill width/height
- openai video provider, the `openai-video-generation` alias, its
  config template and migration entries, and Together's openai/sora-2*
  rows removed
- gemini: Veo 3.1 Fast rates 10/12/30 cents per second for
  720p/1080p/4K, Veo 3.1 Lite accepts reference images, URL image
  inputs are fetched server-side through the SSRF-guarded fetch
- together: drop nine models retired upstream, add eighteen from the
  live listing; per-second models are estimated from the catalog rate,
  clamped to remaining credit and billed at the cost Together reports
  on the job; tier-sized models take resolution/ratio;
  input_reference/last_frame map onto keyframes; generate_audio is
  forwarded
- byteplus: Seedance 2.5 (dreamina-seedance-2-5-260628) with per-model
  reference-image caps
- util/imageInput: string-level image helpers shared by the image and
  video drivers; ai-image/inputImage re-exports them unchanged
- puter.js types: provider and generate_audio options; docs: txt2vid
  page rewritten with per-provider model tables, unified options and
  four new playground examples

Known follow-up: Veo returns a key-protected Google file URL, so the
default clip cannot be played directly by a browser until the provider
fetches it server-side.

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-09 14:56:17 -07:00
404oopsandClaude Fable 5.1 c8f4906af4 fix(ai): keep per-provider failures when every chat route fails (#3836)
When the chat fallback chain is exhausted, the driver already records
each attempt (model, provider, status, code, message, timeout) in the
error's `fields.attempts`, but the alarm keyed on the classified message
alone and the alarm client printed the error at inspect depth 2, so the
log and Slack line read `internal_error:All providers failed` with
nothing about which providers failed or why. Deduped repeats printed
only a count.

- The HTTP alarm gate now attaches an HttpError's `fields` to the alarm
  under a single `details` key. One key can't shadow the gate's own
  request fields, and a repeat from another thrower on a shared id
  replaces it instead of merging into it.
- The chat driver logs one warn line per exhausted chain with the
  completion id, the resolved route, the classified code and the
  attempts as JSON, so every occurrence is greppable by trace even when
  the alarm dedupes it. Chains marked `noAlarm` don't log.
- Docs: `fields` reaches both the client and the alarm, so it has to be
  safe to show the caller.

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-09 14:56:11 -07:00
Reynaldi Chernando 435629afc5 Add gpt 2.5 image (#3839) 2026-09-09 14:29:41 -07:00
404oopsandClaude Fable 5.1 1cb5472c4c fix(ai): canonicalize image parts once and translate them per provider
The SDK shorthand sends `{ image_url: { url } }` with no type. Claude
forwarded it untouched (Anthropic: "content.1.type: Field required"),
the OpenAI/Azure Responses providers sent a Chat Completions part the
Responses API rejects, and Mistral emitted snake_case `image_url` where
its SDK validates `imageUrl`. All three only appeared to work because
the fallback loop re-served them through OpenRouter.

- utils/mediaParts: canonicalise every inbound media part (untyped
  shorthand, bare string URLs, Responses input_image, Anthropic image
  blocks, Gemini inline_data) to the Chat Completions form in the
  driver, so providers translate from one shape
- Claude → Anthropic image blocks (url / base64 sources), copy-on-write
- Responses processor → input_image with a string URL and detail, and
  copies messages instead of mutating the caller's objects
- Mistral → camelCase imageUrl; the test that asserted the old shape
  is corrected
- driver: 400 up front when the catalog says the model has no image or
  video input; resolve puter_path parts per attempt for providers
  without their own upload path (Claude keeps the Files API)
- move the Moonshot http→data-URL inliner to utils/inlineImages and
  apply it to Grok-via-Azure and Gemini, whose upstreams cannot fetch
  http image URLs

Fixes #3409

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 15:23:40 +02:00
Daniel Salazar 2f70cd1a14 feat(email): sendTransactional alias and streaming file inputs (#3812)
- puter.email.sendTransactional is the new name; send stays as a
  deprecated alias with the same arguments and result
- fileInput: openFileInputStream / resolveFileInputEntry expose the
  ACL-checked FS read as a stream; loadFileInput wraps them
- EmailAttachment accepts a `path` the transport streams on its own
2026-09-06 16:41:02 -07:00
404oopsandClaude Fable 5.1 ded3d3695b fix(ai): remove retired Imagen 4 and legacy Moonshot models
Google shut down imagen-4.0-fast/standard/ultra in the Gemini API on
2026-08-17; live calls now return 404 "not found ... or is not supported
for predict". Drop the three entries, the generateImages code path that
only they used, and their tests; the integration test moves to
gemini-2.5-flash-image.

Moonshot's live /models listing and the Kimi pricing docs now carry only
kimi-k3, kimi-k2.7-code(-highspeed) and kimi-k2.6. Drop kimi-k2.5 and the
whole moonshot-v1-* family and repoint the unit and integration tests at
current models. kimi-k2.5 still resolves through OpenRouter.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-05 22:22:16 +02:00
Daniel Salazar b784b51cf3 fix: harden the events stack for flag-on (#3752)
* fix: harden events dispatch, single delivery and KV share handles

Dispatch: a filtered subscription used the anchor path stored at subscribe
time, so renaming or moving the anchor folder silently ended its deliveries;
dispatch now resolves the anchor's live path from the event's own ancestor
chain. A move out of a watched folder now reaches that folder's subscribers,
with `from` only for rows that watched the source side. Gap markers are
authorized like deliveries and coalesced per subscription and subject instead
of fanning per lost event. Session subscriptions: the per-socket cap decides
on the write, not before it; an orphaned watched-set token heals on refresh;
durable rows keep their watch window when a session subscribe touches the
same keys. `self` is false when the acting user is unknown.

Single delivery: a subscription in backoff or suspended with a backlog pinned
the sweeper's head and starved everyone behind it — the sweep now defers it.
Only a settled handler run bills a delivery. A socket-only account row no
longer wedges after two attempts nobody received. The lease is twice the
handler timeout; remote candidates have their own attempt counter; the region
depth reconcile runs once a minute region-wide with a bounded scan.

KV share handles: a grantee no longer sees the owner's namespace and absolute
prefix on the subscribe answer or listing, nor in the delivery token; revoking
a wider handle retires the handles it covers; minting the same handle twice
returns the existing one, after the delegation check; a row whose event
cannot be re-based onto its handle is dropped rather than delivered raw.

* fix: presence survives replication, long sessions and region churn

One presence item per (user, app) with per-region map fields lost a region
whenever two regions joined inside the replication window, and nothing ever
put it back. Presence is now one item per (user, app, region): each region
writes only its own, a leave or repair retires it conditionally on its own
write stamp, and a read is a prefix query. Items carry a 48 h ttl refreshed by
a claim-gated write off the existing socket renew path, at most once per
12 h, so a tab that stays connected keeps its region in the row. A region
that answered "no socket" or completed a leave releases a shared pin, so a
reconnect on another node rejoins and a flapping client cannot force a
replicated write per cycle. Cached rows expire after a minute; unaddressable
region names are filtered and pruned; relayed acks settle under a bounded
concurrency; the forward queue is bounded in bytes as well as items.

* feat: indexes for the event_subscriptions hot queries

Handler publish, remove and listing, and the hourly expiry and suspension
sweeps, all scanned `event_subscriptions`. Adds (app_uid, handler_name),
(expires_at) and (suspended_at, id), guarded on every engine. Existing
migrations: the postgres widens are now guarded so a boot does not take an
exclusive lock for a no-op, the kv_share_handles grantee FK gets an index,
the sqlite notification rebuild is transactional and idempotent.

* fix: notification writes go through the registry

The driver's `create` bypassed the type registry, producing uncatalogued
rows with no size bound; it now requires a registered type, caps the payload,
and answers 400 rather than 500 for a bad one. `mark_acknowledged` emits the
ack other tabs listen for, and only when a row was actually changed.

* fix: the handler scanner, unsubscribe, and the in-tab handler environment

The free-variable scanner skipped arrows inside a declaration's initializer,
so `const ids = event.items.map(x => x.id)` was refused, and treated a name
after a comma in a nested initializer as bound, so a real free variable slipped
through to fail on first delivery. `unsubscribe()` now drops the durable
routing entry so the events socket can close. A broadcast handler running in
the tab gets `user` and `fetch` like the worker gives it. `single` without
a handler name is refused before the round trip.

* docs: events limits, error codes and the background-workers section

Retention is deployment-configured rather than a fixed 14 days, and the
template no longer ships it armed. Documents `events_terminal`, the two
per-event gap reasons, the subject length and listing caps, the `from` field
on moves, and the handle-relative anchor. The sessions manager hides the
background-workers section when the server has none to show.

* feat: a background handler acts as the app does for its user

A handler's `user` was a five-minute access token scoped to the subscription's
`list` grant, which could stat the changed file but not read it, and could
not reach the app's KV or AppData — so an app told that a file was written
could do nothing with it. It now runs with the same authority the app has for
that user in a tab: an app-under-user worker session, one row per (user, app)
named `events:handlers`, visible and revocable in the sessions list. The
`events:background` consent is what authorizes running it unattended, and is
re-checked before every mint.

The wider token exposed two things: puter.js opens a filesystem socket the
moment it has a token, which would have parked the isolate in the app's own
delivery room and steered deliveries at it; the events client now opts out of
sockets (and the per-open bookkeeping) before construction, and is memoized
per token in the isolate. And four filesystem operations assumed a socket
exists; they no longer do.
2026-09-04 17:32:57 -07:00
Daniel Salazar f30baa2a1c feat: the per-app events worker runtime (#3697)
* feat: bake published handlers into a generated events worker

* feat: deploy and address the per-app events worker behind a flag

* test: single delivery end to end through a real local worker

* feat: events workers run their own runtime, in their own namespace

An events worker was being deployed as an ordinary worker: default dispatch
namespace, a `subdomains` row, the router preamble, and an app-scoped worker
token baked in. The public dispatcher resolves any script in that namespace
straight off the hostname, so the worker answered at `<name>.puter.work`, and
the only thing in front of it was an unguessable name plus a check that a
`puter-auth` header was present — which the router never validates. Anyone who
learned the hostname could run an app's handlers with a body of their choosing,
in an isolate holding the owner's token as `me`.

Instead:

- Handlers run on their own runtime (`src/worker/src/events-runtime.js`), which
  provides no `router` and no `me`, owns the single invoke route, and hands a
  handler only `{ event, ctx, user, fetch, ack }`. `user` is built from the
  invocation's delivery token, so a handler acts as the subscriber whose
  delivery it is and nothing wider. The preamble build emits one bundle per
  runtime; the shared half of the template is now included by both.
- The deploy target carries the runtime to prepend, the source to deploy, and
  whether to mint a worker token at all, so an events worker deploys into the
  `events` dispatch namespace from generated source with no token binding, no
  `subdomains` row, and no claim on the owner's worker quota or worker list.
- An invocation carries a key derived from the deployment secret and the script
  name, bound as a secret and checked in constant time inside the isolate,
  which reads it once and drops it before handler code runs.
- Scripts are named after the handler set they contain, so publishing writes
  rows and deploys nothing: a set is deployed the first time a delivery needs
  it, and a changed set is a new script rather than an overwrite of a running
  one. Publish responses keep the shape they had before the runtime existed.
- Invocations reach a worker only through the events dispatcher, which has no
  zone route and requires the internal secret; the backend's own deploy path is
  the rehydrate route the dispatcher calls on a namespace miss. Locally there is
  no dispatcher, so the controller hands the service an in-process transport
  that deploys on miss itself.

The SDK stops allowlisting `puter` as a handler global — a handler that reaches
for an ambient SDK is now refused at publish time, naming `user` instead, rather
than passing the scan and failing on its first delivery.

Requires `events.workerNamespace`, `events.dispatcherUrl` and
`events.internalSecret`; without them nothing is addressable and background
deliveries stay retriable, as they did with the runtime off.

* fix: a handler's delivery token gets through the read routes

An events handler acts as the subscriber through the access token its
invocation carried, but every FS read route refused scoped access tokens
outright, so `user.fs.stat(event.path)` — the design's own example — answered
403 inside the worker. The read-side routes now admit them; the ACL each
handler already runs intersects the token's grant with its issuer's, which is
the check that keeps a token to what it was minted for. The end-to-end suite
asserts the stat from inside the isolate.

* fix: shorthand-method handlers publish as functions

`{ ingest({ event }) { … } }` stringifies without the `function` keyword, so
its source is not an expression and the events worker baked it as a broken
stub — every delivery a retriable 500 until the subscription suspended, with
nothing at publish time to say why. The SDK now gives a shorthand method the
keyword before hashing and sending; getters, setters and computed names are
left for the server-side check to refuse.

* feat: an app's events worker is listable and destroyable

An app with published handlers has an events worker, and hosted deployments
bill it monthly per app, so its owner needs to see it and be able to take it
down. The core announces the lifecycle on the bus — `events.worker.create`
when an app's first handler is published, `events.worker.destroy` when its last
one goes — with the owner as the actor, so pricing can plug in from outside.
`GET /events/workers` lists the caller's workers (paginated, with the script
each set deploys as) and `POST /events/workers/destroy` removes every handler
of an app under the same owner scoping as the handler routes, suspending the
subscriptions bound to them. `puter.events.workers.list/destroy` in the SDK,
a docs page, and a 5 MB cap on an app's combined handler source
(`events_worker_too_large`) so a set that publishes can always deploy.

* fix: harden the events worker runtime for production

- A 4xx is terminal only when it carries the handled marker the runtime (and
  the dispatcher) stamp on every answer that came from a script; an unmarked
  4xx — an edge 404 for a wrong dispatcher hostname, a WAF page — stays
  retriable and is logged, once per script per minute, with the runtime's
  reason header.
- Script names are scoped to this backend's exposed API origin, so two
  backends sharing a namespace never resolve one script with the wrong
  endpoint binding or key. Shape unchanged.
- Each handler is validated in the exact context it is emitted into and the
  whole generated file is compiled once; a source that would break the script
  marks every handler broken instead of deploying a SyntaxError.
- Locally, events scripts live under their own registry key: the public local
  worker host cannot reach them and an ordinary worker cannot take their name.
- A suspended or deleted app owner stops invocations; deploys are throttled
  per app per hour; in-flight deploys are keyed by app and script; the
  upstream deploy call times out; the generated source is size-capped with a
  margin over the publish cap; boot fails when the runtime is on but its
  preamble is not built. Byte-length secret compare, appUid shape check,
  dispatcher URL prefix preserved, wider connection pool.

* feat: background workers are listed in the sessions manager

A user paying for an app's events worker needs somewhere to see it and take it
down. The sessions manager gets a section listing the apps that run event
handlers in the background, with a Destroy action that removes their published
handlers.
2026-09-04 17:32:57 -07:00
Daniel Salazar 3355190a17 feat: add astra to models (#3755) 2026-09-04 15:56:53 -07:00
404oopsandClaude Opus 5 7248951410 fix(ai): make chat fallback reach streamed Claude calls and rank Azure explicitly (#3743)
Maintain Release Merge PR / update-release-pr (push) Canceled after 0s
Notify HeyPuter / notify (push) Canceled after 0s
release-please / release-please (push) Canceled after 0s
* 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>
2026-09-04 11:10:05 -07:00
Daniel Salazar adde6c3a29 feat: notification GUI rides the events surface (PUT-1685) (#3689)
Widen the notif: match filter and fetch scope for a session's own
generic developer/app-user subscribe: today it pins ref to the
session's own uuid, so a row naming an app (handler-suspension
notices, app-bound worker deploys) never matches live and never
replays on reconnect, even though the audience predicate already
grants the holder every such row it owns. The predicate is the
authority and already reruns per row/page after the match, so
widening the filter to it (account is unaffected — it never names an
app) adds no exposure.
2026-09-03 15:39:22 -07:00
Daniel Salazar fe5f81e52e feat: scoped app access to notifications (PUT-1684) (#3688)
An actor holding an app reads the `app-user` rows naming that app, plus its
`developer` rows when the holder owns it. `account` rows reach no app, and a
slice an actor may not see comes back empty rather than refused. The audience
predicate becomes the enforced read path in the same change that lifts the
blanket app-actor 403, layered behind an audience/app_uid SQL scope; two-segment
`notif:` subjects expand server-side from the actor's own app, so an app can
never name another app's uid.

No feature flag: `audience` defaults to 'account', so every pre-registry row is
default-denied to app actors and the backfill can only narrow.
2026-09-03 15:39:22 -07:00
404oops 1b777cd760 Merge pull request #3730 from HeyPuter/filipkujundzic/put-1620-video-generation-timeouts-return-500-and-page-on-call
fix: stop video generation timeouts and failures from paging (PUT-1620)
2026-09-03 22:54:58 +02:00
Reynaldi Chernando 644f59f9dd gemini 3.8 flash direct integration 2026-09-03 22:18:02 +07:00
404oopsandClaude Fable 5.1 627a5b2d5e fix: stop video generation timeouts and failures from paging (PUT-1620)
A video job that outlived its poll window, an SDK request that timed out,
or a Veo operation that finished with an error all reached the HTTP error
handler as plain Errors. Each became an unhandled 500 with critical
severity and paged on-call for what is the provider's pace or the
provider's fault.

Video providers now share one poll loop that gives up with a 504
`upstream_timeout`, treats a transient poll failure (timeout, dropped
connection, 408/429/5xx) as a missed poll rather than a failed job, and
stops polling with a 400 `client_aborted` when the caller disconnects, so
nothing is metered for a clip nobody will receive. The driver controller
exposes the disconnect as an `abortSignal` on the request context. The
window is ten minutes for every provider; Together and BytePlus move up
from five.

Failed jobs are classified: content-filter refusals become a 400
`bad_request` with `errorCode: moderation_flagged`, rejected parameters a
400 `upstream_bad_request`, and anything else a 502 `upstream_failed`,
each carrying the provider's own code. Veo's filtered output keeps
`disallowed_value` and gains the same `errorCode`. The sanitizer and
content-filter pattern move from the Replicate provider into a shared
util so image and video agree.

Status-less SDK connection timeouts are translated to a 504
`upstream_timeout` at the driver boundary, and the chat driver records
them per attempt so an all-timeout chain is a 504 and a mixed chain is
`upstream_failed` instead of an `internal_error` 500. The Together chat
client gets the same ten-minute request timeout as the other providers.

The OpenAI video provider is left alone beyond an import path: its API is
scheduled to shut down on 2026-09-24.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-03 16:47:16 +02:00
Neal Shah af27115f68 Attempt 1: Fix PUT-1625 (#3727) 2026-09-02 21:36:04 -04:00
404oopsandClaude Fable 5.1 d89dc347a2 fix: keep the credit-bounded max_tokens on the context-length retry (PUT-1628) (#3718)
OpenRouter and Together reject a request whose prompt plus max_tokens
overflows the model's context window. Both providers retried by deleting
max_tokens, which threw away the output cap the credit gate had sized to
the caller's remaining balance and let the retry run to the model's full
output limit with no second gate and no new hold.

The retry now goes through a shared helper that sizes a new cap from the
window and input count the rejection reports, falling back to the model's
declared context and a doubled prompt estimate, and never exceeds the cap
the gate set. When no window can be determined or no output fits, the
original rejection is rethrown instead of retrying uncapped. The rejected
params are copied rather than mutated, so the first attempt's record is
not rewritten after the fact.

Also corrects the estimator's own comment, which described the mean of
two approximations as a deliberate halving.

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-02 13:28:36 -07:00
404oopsandClaude Fable 5.1 fd7b517448 fix: classify Replicate prediction failures instead of 500ing (PUT-1608) (#3719)
A Replicate prediction that ran and ended `failed` reaches the provider as a
plain Error with no HTTP status, so the driver-boundary translator could not
classify it and it surfaced as an unhandled 500, a critical alarm, and an
on-call page. Most of these are the model's content filter refusing the
user's prompt.

Wrap the run call and classify the failure: content-filter refusals become
a 400 with `errorCode: moderation_flagged` (the code chat refusals already
use); anything else becomes a 502 `upstream_failed`, which the alarm gate
skips. Status-bearing SDK errors pass through untouched so the boundary
translator keeps handling them. Upstream messages are stripped of markup and
bounded so an HTML error page can no longer ride into a response body or an
alarm signature.

Documents the codes callers can now act on in the txt2img reference.

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-02 11:51:08 -07:00
Daniel Salazar d0ee19a2c2 chore: remove dead notification surfaces (PUT-1669) (#3673)
Maintain Release Merge PR / update-release-pr (push) Canceled after 0s
Notify HeyPuter / notify (push) Canceled after 0s
release-please / release-please (push) Canceled after 0s
2026-09-01 22:04:33 -07:00
Daniel Salazar 5f37817a5c feat: notification type registry and audience rules (PUT-1665) (#3671) 2026-09-01 22:04:32 -07:00
404oopsandClaude Fable 5.1 6478a8f47d Add Claude Fable 5.1 (#3711)
Register claude-fable-5-1 in the Claude catalog and gate it in the
provider the same way as Fable 5: no sampling params, effort via
output_config, adaptive thinking with summarized display. The bare
claude-fable / claude-fable-latest aliases move to 5.1, matching how
the Opus aliases moved when Opus 5 landed.

Fable 5.1 keeps Fable 5's $10/$50 per MTok, 1M context and 128K output,
but bills cache reads at 0.025x input instead of the 0.1x every other
Claude model uses, so the catalog row carries its own rate and a test
pins it.

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-01 15:34:49 -04:00
Daniel Salazar 7584dffc4b fix: metering for ai (#3703) 2026-09-01 09:11:04 -07:00
404oops a27852de0a Merge pull request #3655 from HeyPuter/FK/normalize-openai-format
Maintain Release Merge PR / update-release-pr (push) Canceled after 0s
Notify HeyPuter / notify (push) Canceled after 0s
release-please / release-please (push) Canceled after 0s
Comprehensive Normalization.
2026-08-31 22:24:34 +02:00
404oopsandClaude Fable 5 7e1cdefd98 feat(ai): backfill provider catalogs from live /models listings
A live audit of every keyed provider's /models endpoint against the hardcoded
catalogs found no stale entries but large gaps. This backfills them under four
rules: nothing vendor-deprecated, nothing without a price confirmed on the
vendor's official pricing page (each entry's source was recorded during
review), nothing absent from the live /models listing, and nothing that fails
a live routing probe.

Added: 46 Alibaba entries (qwen3/3.5/3.7/3.8 families, VL/omni/MT lines, and
Model Studio's hosted GLM/DeepSeek/Kimi third-party models) plus 9 dated
aliases; OpenAI chat-latest and gpt-4o-2024-11-20 plus 16 snapshot aliases;
Gemini gemma-4-31b-it and gemma-4-26b-a4b-it (vendor-documented free tier)
plus rolling -latest aliases; Mistral-hosted zai-glm-5-2 and a
mistral-medium-3.5 alias; deepseek-v4-flash-vision-exp; glm-5.3-flash.

Culled by the rules: 15 vendor-deprecated OpenAI entries (the 3.5/4/4-turbo
legacy line, gpt-4o-2024-05-13, o1-pro, four chat-latest predecessors, the
5.x codex line — deprecations page, most shut down 2026-10-23) and dated
aliases onto the deprecated o1/o3-mini/o4-mini; qwen3-vl-flash-2025-10-15
(live routing probe returned upstream 400 twice). Tiered Alibaba prices are
encoded at the base tier and busy-hour rates where time-of-day priced, noted
in comments.

Every surviving addition was verified end-to-end through a local deployment:
58/58 answered a live prompt, including all 46 Alibaba entries and every
spot-checked alias.

Not changed, flagged for maintainers: pre-existing o1, o3-mini and o4-mini
entries are now vendor-deprecated (shutdown 2026-10-23); the pre-existing
deepseek-v4-flash/-pro prices no longer match DeepSeek's current pricing
page; gemma-4 emits its own <thought> markup inline in content.

Co-Authored-By: Claude Fable 5 (1M context) <noreply@anthropic.com>
2026-08-30 20:50:46 +02:00
404oopsandClaude Fable 5 4199a2506d feat(ai): forward Mistral prompt_mode so magistral can emit separable thinking
Live-probing magistral-small-latest showed the model inlines its reasoning as
answer prose in a flat string — no ThinkChunk content, no markers, nothing a
client can separate. Mistral's chunked thinking shape is requested via
`prompt_mode: 'reasoning'`, which the provider previously dropped on the
floor: there was no way to even ask for it.

`custom.prompt_mode` now forwards to the SDK's `promptMode`, following the
BytePlus custom-params precedent. Opt-in rather than a default because the
API rejects the mode where the account/model lacks it ('Reasoning prompt
mode is not enabled for this model', code 3051) — verified end-to-end: the
3051 travels back through the stack, which also proves the parameter is
delivered. The moment Mistral enables the mode, the ThinkChunk content flows
into the existing splitter and comes out as `message.reasoning` and
`reasoning` stream chunks with no further changes.

Co-Authored-By: Claude Fable 5 (1M context) <noreply@anthropic.com>
2026-08-30 20:50:46 +02:00
Juan Fernando Castro 81d700d146 fix: publishing a directory takes ownership, not write (PUT-1619) (#3654)
Maintain Release Merge PR / update-release-pr (push) Canceled after 0s
Notify HeyPuter / notify (push) Canceled after 0s
release-please / release-please (push) Canceled after 0s
Creating a hosted subdomain gated `root_dir` on `write`, and hosting serves
everything under that directory with the ACL deliberately bypassed. So a
recipient of a `write` share could point a `*.puter.site` subdomain at the
owner's folder and make the subtree world-readable — continuously, covering
files the owner added later, with the row under the recipient's account where
nothing the owner can list would show it. `update` had the same gate for a
changed `root_dir`.

`#checkPublishAccess` now decides both: the actor's own tree still takes
`write`, anyone else's takes `manage` — "Can edit & share", the level that
delegates the decision.

Keyed on who owns the entry rather than asking for `manage` outright, which is
what the ticket proposed. `manage`'s is-owner implicator declines to answer for
app actors, so a flat `manage` would refuse every app publishing a directory
its user handed it, with no way for the app to obtain the grant. The write
check still runs first — it is what masks a directory the caller cannot see as
a 404 — and `manage` satisfies every lower mode, so the order costs a
manage-holder nothing.

The GUI's Publish As Website item reuses the own-it-or-`manage` answer it
already computes for sharing, so it is not offered where this would refuse.
Docs state the rule on `hosting.create()` and in `share()`'s level list.

Regression tests fail without the driver change: a write-share recipient is
refused on create and on repointing an existing subdomain, while `manage` and
the actor's own directory are accepted.
2026-08-29 01:46:14 -07:00
Daniel Salazar 9fab4742c9 fix: complete the malformed-input guards, including the two paths still live (#3663)
Review of the previous change found three of its claims unmet.

The image-generation crash it reported fixed is still reachable. The assert was
scattered across three helpers, and Gemini and OpenAI call `isHttpUrl` directly
on `input_images` without going through any of them — so two of seven providers
still 500 on a non-string. `isHttpUrl` now refuses a non-string itself, and the
shape is settled once in `ImageGenerationDriver.generate`, where the driver call
arrives, rather than per helper. That also covers `input_images` that isn't an
array, which produced a different crash per provider.

The sixth case in the ticket, previously unlocated, is
`Messages.js` reading `tool_call.function.name` with no guard — reachable with
`{"messages":[{"role":"assistant","tool_calls":[{"id":"x"}]}]}`. Guarded, along
with the same shape in `make_claude_tools`: a TypeError there carries no status,
so the retry loop reads it as a provider failure and marks the route unhealthy
for every caller.

`#hardExpiryFromExpiresIn` returning null for a bad type moved the failure past
the session INSERT, leaving an orphaned non-expiring row and still answering
500. Reverted; the controller guard is the fix, now covering fractions,
negatives and unparseable durations rather than only wrong types.

Also: the batch write handlers check that the body is an array but not what is
in it, so a null element 500s the same way; `#requireObjectBody` accepted an
array despite its name; `handleCreateAccessToken` destructured a body that may
be absent; and two AGPL notices had been rewrapped with a Markdown link.
2026-08-28 11:14:30 -07:00
Daniel Salazar e460e9034c fix: answer 400 instead of 500 on four malformed-input paths (#3662)
Each of these read a field off caller input that wasn't the shape the code
assumed, threw a TypeError, and was served as a 500 with a critical page.

- POST /fs/write and /startWrite: a request whose body never parsed left
  `req.body` undefined, and the first read of `fileMetadata` threw.
- POST /drivers/call, image generation: `input_image` / `input_images` entries
  are documented as strings but nothing checked, so a number or an object
  reached `.startsWith`. Type confusion on caller input, so reachable on
  demand rather than by accident.
- POST /auth/create-access-token: `expiresIn` went to the expiry parser
  unvalidated, where anything but a string or a number has no `.trim`.
- POST /login/wait: destructuring `session` out of an absent body threw.
  Same class as the POST /login body ticket; that one covers /login itself.
2026-08-28 10:22:51 -07:00
404oopsandClaude Fable 5 07998c32ed test(ai): point the provider-level replay test at the real driver fallback test
The comment claimed the fallback loop cannot be driven from a test — false
since ChatCompletionDriver.test.ts gained a two-attempt fallback test that
drives the actual loop. Say where that test lives instead.

Co-Authored-By: Claude Fable 5 (1M context) <noreply@anthropic.com>
2026-08-28 01:09:14 +02:00
404oopsandClaude Opus 5 04d12d9524 fix(ai): route Mistral streamed thinking to the reasoning channel, unconditionally
Third triple-check round. Ten findings were put through independent skeptics
first; six did not survive — pre-existing on main, inert, or resting on a false
premise — and are not acted on here.

Mistral streamed thinking now goes to `reasoning` on every path. The previous
commit gated the split, which made this the only place in the repo where
chain-of-thought reached the visible text channel, and made Mistral the only
provider whose streamed chunk *types* depend on a response-format flag. Every
other reasoning path routes thinking to `reasoning` unconditionally —
ClaudeProvider's thinking_delta, the DeepSeek/OpenRouter rename, and this
branch's own Responses summary-delta handler. Removing the gate restores that
uniformity and makes the documented promise that streaming is unaffected by
normalization true again; the two opposing tests collapse into one that runs the
same fixture with and without `normalize` and asserts identical event streams.

Docs stop claiming older models are unchanged. Four reasoning fields were made
consistent across all models, ungated, and one of them removes a field: on
non-streaming responses `message.reasoning_content` is now `message.reasoning`.
chat.md gains a table naming all four so a caller reading `reasoning_content`
learns why it disappeared, instead of reading that nothing changed for them.

Adds the driver-level fallback test. Writing it surfaced that the invariant it
was meant to assert is false and always was: the driver rewrites string
`content` into text blocks in place on the caller's own messages
(`normalize_single_message`, pre-existing) before any provider runs. The test
now asserts what is true and load-bearing — both attempts receive the same array
reference, and the reasoning artifacts survive attempt 1 so attempt 2 can still
replay them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-28 01:09:14 +02:00
404oopsandClaude Opus 5 216e8d68ed fix(ai): scope the normalized flag, gate the Mistral stream split, correct four doc claims
Acts on a second triple-check audit.

`normalized` means one thing. The driver stamped it on both the OpenAI-shape
path and the legacy `response.normalize` path, which converts toward Anthropic
blocks — so a caller could get `normalized: true` alongside array content, and
the flag told them nothing they could branch on. The legacy branch no longer
sets it. That branch is reachable only by a direct driver call with
`response.normalize` and no `normalize`; the four wire routes pin
`normalize: false`, which skips it.

Mistral streaming, split by concern. Flattening a reasoning model's chunked
`delta.content` to a string is a correctness floor and stays ungated — the
shared handler passes the value straight to `addText`, so an array reaches the
caller as stringified objects. Splitting the thinking text out into a
`reasoning` delta is the dialect change and now sits behind the policy gate
like the non-streaming remap. On the native path the thinking text is kept
inline rather than dropped.

Mistral `finishReason` is deleted only once its value carried over. The delete
ran unconditionally, so a non-string `finishReason` with no `finish_reason`
left the choice with no finish reason at all.

Four doc claims corrected against the code paths they cover: `content` is
string-or-null on normalized responses (tool-only turns carry no text, and the
`// always a string` example comment was wrong); `reasoning_details` is not
scoped to normalized responses, since Responses models emit it either way; and
the release-date rule depends on the serving provider's own dates — OpenRouter
derives them from its live API, so models newly listed there from 2026-09-01
normalize by default.

Adds the test the copy-on-write fix was actually for: one messages array sent
through two sequential calls, asserting the caller's array is untouched and
both attempts carried the thinking signature. That is the fallback hazard; the
harness wires one provider per model, so the fallback loop itself cannot be
driven from a provider test.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-28 01:09:14 +02:00
404oopsandClaude Opus 5 c1b420e480 fix(ai): gate the Mistral remap, unify the reasoning join, stop mutating caller messages
Acts on a triple-check audit of this branch.

Gate the Mistral dialect remap. The camelCase→snake_case rewrite and the
chunked-content flattening were firing for every Mistral call regardless of
`normalize` or the cutoff, deleting `finishReason` and `message.toolCalls` out
from under any caller reading them. Both now sit behind the policy resolution
the driver already used, extracted as `shouldPresentAsOpenAI` so the provider
and the driver cannot drift. The streaming chunk-array split stays ungated:
handing an array to `addText` is a plain bug, and streamed chunks are
provider-uniform by design. The conformance matrix now passes `normalize: true`,
which is the contract it was always testing.

Unify the reasoning join. Three code paths produced two separators while one
doc sentence described them all: the coercer joined thinking segments with '',
the Responses handler and Mistral with '\n\n'. The coercer now matches, and
chatresponse.md's claim is true for every path it covers. Text blocks still
join with '' — Anthropic splits prose mid-sentence across them.

finish_reason is an open set. chat.md's normalize bullet and the SDK
ChatMessage typedef still declared a closed four-value set, contradicting the
documented pass-through of unmapped vendor reasons and the coercer that
implements it.

Stop mutating caller messages. Both reasoning-replay input paths deleted
output-only fields from the caller's own message objects, which the driver
reuses across fallback attempts. Both strip a copy now; tests pass a frozen
message through each.

Drop three dead things the type cleanup left: the no-op ChatProvider
checkModeration stub (no subclasses, no callers — its removal restores a
pre-existing baselined TS2420), the redundant second normalizeReasoningContent
call in BytePlus and ZAI, and the coercer's bare-string branch that no provider
reaches. A bare string now passes through by reference instead of being
coerced.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-28 01:09:14 +02:00
404oopsandClaude Opus 5 74640d4e83 fix(ai): flatten Mistral reasoning models' chunked content
`magistral-*` returns `message.content` as a ContentChunk[] rather than a
string, with the thinking text nested one level deeper inside `thinking`
chunks. The camelCase remap did not touch it, so a non-streamed magistral
response reached the caller as an array with no `reasoning` — the one case
left where a provider did not produce the equalized shape this branch
promises. Streaming had the matching bug: the chunk array was handed to
addText, which would have stringified it into the text stream.

Both paths now split chunked content into a string `content` plus a
`reasoning` string, joining multiple thinking chunks with a blank line as the
Responses handler and the Anthropic coercer do. The streaming fix rides the
existing Mistral-only `chunk_but_like_actually` hook, so no new deviation is
introduced.

The conformance matrix had no Mistral reasoning fixture, which is why it
missed this; it now has one carrying chunked content, verified to fail
without the flattening.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-28 01:08:48 +02:00
404oopsandClaude Opus 5 a6830f341a fix(ai): round-trip reasoning artifacts and pass unmapped stop reasons through
Closes the reasoning gaps left open by the normalization work.

Reasoning replay. The coercer dropped Anthropic thinking-block signatures and
the Responses handler dropped reasoning item ids/encrypted_content, so a
normalized reasoning turn could not be replayed — Anthropic rejects an
extended-thinking tool-use continuation whose thinking blocks lost their
signature. Both now ride `message.reasoning_details` verbatim, and both input
paths accept them back: ClaudeProvider splices the blocks ahead of the content
(Anthropic requires them to lead), and the Responses input processor expands
them into standalone `reasoning` items. Output-only fields a replayed message
carries (`reasoning`, `refusal`, `normalized`) are stripped on both paths,
since neither upstream accepts them. The docs caveat recommending
`normalize: false` for agentic Claude loops is gone; it is no longer true.

Unmapped stop reasons. chatresponse.md promised a vendor `finish_reason` with
no OpenAI analog "passes through unchanged" — true for the Mistral remap, false
for the Anthropic coercer, which discarded it. Anthropic's `pause_turn` means
"continue this turn", so flattening it to `stop` destroyed the signal. The
coercer now passes unmapped values through verbatim, matching both the doc and
the Mistral path, and the docs gain the full Anthropic stop-reason table.

Reasoning summaries. Multi-part summaries joined with '' instead of a blank
line, and the streaming Responses path emitted no reasoning at all;
`response.reasoning_summary_text.delta` now feeds the same `reasoning` stream
channel the chat-completions handler uses.

Types. `text?: string & { verbosity?: ... }` was an uninhabitable intersection
(providers read `text?.verbosity` as an object), and the verbosity enum was
`'concise' | 'detailed'` where OpenAI accepts `'low' | 'medium' | 'high'`.
Adds `reasoning`, `reasoning_details`, and `refusal` to the SDK ChatMessage
typedef.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-28 01:08:48 +02:00
404oopsandClaude Fable 5 548d26f884 test(ai): cover Hoonify in the provider-consistency matrix
Adds the upstream Hoonify provider (feat #3499, merged after this
branch was cut) to the cross-provider output conformance suite. It
speaks the OpenAI chat-completions dialect and conforms out of the box.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-28 01:08:48 +02:00
404oops 9b67a2b1dd Add AI response normalization controls
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.
2026-08-28 01:08:47 +02:00
Nariman Jelveh 7472002cf3 feat: add Hoonify AI chat provider (#3499) 2026-08-26 15:39:29 -07:00
Juan Castro 07e73efa40 Merge remote-tracking branch 'origin/main' into juancastro/put-1568-rename-srcguisrchelpers-files-to-camelcase 2026-08-25 15:10:29 -04:00
404oops 2a45a10854 Merge pull request #3630 from HeyPuter/FK/dedupe
refactor: dedupe model catalog aliases and stop mutating shared catalogs
2026-08-25 18:07:27 +02:00
Juan Castro 4a5d689af7 refactor: rename GUI helper files to camelCase
src/gui/src/helpers was mostly snake_case, disagreeing with itself and
with the camelCase file convention in AGENTS.md. Rename the 45 offenders
with git mv and repoint every import specifier at the new paths.

Files only — exported symbols are unchanged. applyItemAddedToContainers.js
still exports apply_item_added_to_containers and no call site is edited
beyond its import line; those identifiers cross module boundaries and some
hang off window, so renaming them is a separate, riskier change.
2026-08-25 10:49:59 -04:00
Daniel Salazar 764ce2f8e2 feat: rep score middleware gateway (#3639) 2026-08-25 07:11:41 -07:00
404oopsandClaude Fable 5 308b78e96c refactor: dedupe model catalog aliases and stop mutating shared catalogs
- Add modelLookupNames() and route every provider's list() through it so
  repeated or self-referential aliases can't double-list a model.
- Stop the chat/image/video drivers' #buildModelMap from writing through
  to the module-level catalog constants (ids/aliases accumulated across
  instantiations); bucket copies preserve the models() wire shape.
- Remove self-aliases and exact duplicates from the claude, deepseek,
  groq, mistral, and openai catalogs; fold the duplicated
  gemini-3.7-flash entry — this corrects the live cached-token price
  from 8 to 7.5 and the knowledge date to 2026-03 (the previously
  effective first entry carried stale values).
- ClaudeProvider: derive default max_tokens from the resolved entry
  instead of a second name-based lookup that capped dated ids at 4096.
- Add cross-provider catalog invariants (modelCatalogs.test.ts) with a
  disk-scan registration guard, plus regression tests for catalog
  non-mutation, alias-dedup routing, wire shape, and max_tokens ceilings.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 03:42:47 +02:00
404oops 58794f4f76 feat: add MetaProvider for Muse Spark models and integrate with ChatCompletionDriver (#3616)
Maintain Release Merge PR / update-release-pr (push) Canceled after 0s
Notify HeyPuter / notify (push) Canceled after 0s
release-please / release-please (push) Canceled after 0s
- Implemented MetaProvider to interface with Meta's Muse Spark models.
- Integrated MetaProvider into ChatCompletionDriver, allowing for dynamic model selection.
- Added unit and integration tests for MetaProvider to ensure functionality and reliability.
- Updated documentation to include Meta as a supported vendor for AI models.
2026-08-20 15:09:56 -04:00
Daniel Salazar 9debdffc5d fix: email send frequency + ai ordering (#3604)
Maintain Release Merge PR / update-release-pr (push) Canceled after 0s
Notify HeyPuter / notify (push) Canceled after 0s
release-please / release-please (push) Canceled after 0s
2026-08-18 18:13:55 -07:00
b795b219a8 PUT-1497: Share file link sharing and notifications (#3595)
Maintain Release Merge PR / update-release-pr (push) Canceled after 0s
Notify HeyPuter / notify (push) Canceled after 0s
release-please / release-please (push) Canceled after 0s
* refactor(share): move share notifications into their own service

* feat(share): invite an address with no account, and email it

* feat(share): surface pending invites in the SDK and share dialog

* fix(share): unreachable revoke confirmation, and double-encoded labels

* feat(share): budget share announcements, group them, and let people block senders

Sharing had one defence against noise: a 15-minute quiet window per (sender,
recipient) pair, which dropped the second share rather than folding it in.
Twenty senders each under their own window could still bury someone, and there
was no way to make one of them stop.

Announcements are now budgeted on two axes through the existing sliding-window
limiter: 1 per 15 minutes and 20 per day from one sender, and 10 per hour /
50 per day to one recipient from anyone. Over budget the share still succeeds
and the recipient's notification is still brought up to date — only the
interruption is dropped. Invite email to an address with no account is budgeted
the same way, keyed on a hash of the canonical address.

Notifications now fold across senders: a new share rewrites the notification
the recipient hasn't dismissed, so "alice and bob shared 5 items with you"
replaces a stack of five. The record is written even when suppressed, so the
count is right whenever they next look.

Blocking is a new `user_block` table with enforcement in ShareService: a blocked
sender's share is refused with `recipient_not_accepting_shares`, spends no
quota, and writes no row, and their unclaimed invite is dropped when the address
is confirmed. Existing access is untouched — that is what revoke is for.
Managed from a Blocked people card in the dashboard's Security tab.

Also publishes the sharing limits, including the ones already on this branch
that were never documented.

* fix(share): name the item in share email, instead of 'an item'

* fix(share): make the invite lifecycle canonical, authorized, and race-safe

* refactor(email): drop EmailClient.isConfigured; callers read config.email

* feat(share): batch share email into a per-recipient digest, durably

* docs(share): document the share error codes; steady the disk migration tests

* fix(share): log why a digest wasn't sent, and recover orphaned ones

* feat(share): email recipients about shares by default, with a way to decline

Share email was off unless a deployment opted in, which meant an account
holder was told about a share in the app only. It is now on unless
`share_email_notifications` is set to false.

The reason it defaulted off was that nobody could decline. So this also
honors `user.unsubscribed` — the account-wide opt-out the /unsubscribe page
already writes and app feedback already respects, which share email ignored —
and the digest carries that link. Sharing and the in-app notification are
unaffected by it; only the mail stops.

The link is composed in the template around an interpolated uuid rather than
passed pre-built: Handlebars escapes interpolated values, so a whole URL came
out as `user_uuid&#x3D;…`, which browsers decode but link scanners and older
mail clients need not.

* fix(share): count every shared file in the digest, not just the first

* feat(share): let a recipient refuse shares from everyone

Blocking answered "not from this person" but had no answer to "not from
anyone", so the only way to stop a stream of unwanted shares was to name
each sender after they had already reached you.

Stored as a key in the user row's existing `metadata` blob rather than a
column: the share path already holds the recipient's row by the time it
asks, so reading it costs nothing, and a one-bit preference doesn't earn
a migration per dialect. `updateMetadata` merges and refreshes the cached
row, so the switch bites on the very next share.

Refusing everyone reports the same code as refusing one person — which of
the two it is is the recipient's business, not the sender's. Enforced at
both moments the per-sender block is: when the share is issued, and when
a pending invite is claimed. The per-sender list is untouched while the
blanket switch is on, so turning it off restores what it hid.

`GET /share/blocks` now carries `all`; `POST`/`DELETE` take `{ all: true }`
beside the existing `{ username }`. Managed from the same Blocked people
card in the dashboard's Security tab.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(share): keep the digest sweep off a window that still has a timer

The sweep treated an entry as orphaned the moment its window closed, which
is also the moment the node that armed it fires. Claiming an entry is only
exclusive among flushers that can see each other's deletes, so the two
could each claim a share of the same digest and both send. It now waits
out a grace period first, which costs a genuinely stranded digest that
much delay and nothing else.

Both digest listings were capped at 200 with no word when they hit it — a
truncated flush sends a digest that undercounts and reads as complete.
The cap is named and logged.

Also: `#emailHolder` still described share email as off by default, which
it stopped being; the config doc said the batch window defaults to 60s
when it is 90; and the two tests that need several calls inside one window
were racing a 50ms window across four sequential round trips, so they
failed under full-suite load rather than on the behaviour they cover.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(share): stop double-encoding the recipient in two dialog messages

`i18n()` encodes what it returns, replacements included, so encoding the
recipient first showed the entities to anyone whose address or username
contains one. Same pattern already fixed two lines above.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(notification): widen the mysql shown/acknowledged columns

Both arrived from the v1 schema as `tinyint(1)`, where they were flags.
The backend rework changed the writes to a unix second; sqlite (`INTEGER`)
and postgres (`bigint`) took it and mysql did not, so on mysql every
`markShown` and `markAcknowledged` has failed with
ER_WARN_DATA_OUT_OF_RANGE and left the column NULL. Dismissing a
notification never stuck — the unacknowledged count never moved and one
already delivered came back on every reconnect.

No backfill: every reader tests `IS NULL` / `IS NOT NULL` only, so a
legacy `1` keeps meaning "yes" once widened. Guarded on the current type,
because changing a column type copies the table and this directory
replays on every boot.

Not reachable from the test suite — it runs against sqlite and postgres,
both of which already have the right type. Verified by hand against mysql:
`/notif/mark-read` and `/notif/mark-ack` now persist, and a dismissed
share notification is no longer the one a later share folds into.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Daniel Salazar <daniel.salazar@puter.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 09:36:33 -07:00
Anany Singh 95d797b047 feat(workers): expose app_uid in puter.workers.get and puter.workers.list (#3596) 2026-08-18 00:05:17 -07:00
404oops 9604cc3f23 feat(ai-chat): overhaul provider model catalogs to match current APIs (#3586)
Maintain Release Merge PR / update-release-pr (push) Canceled after 0s
Notify HeyPuter / notify (push) Canceled after 0s
release-please / release-please (push) Canceled after 0s
2026-08-17 14:06:44 -04:00
Ayush tiwari fc861da89f feat: add Gemini 3.7 Flash (#3587)
Maintain Release Merge PR / update-release-pr (push) Canceled after 0s
Notify HeyPuter / notify (push) Canceled after 0s
release-please / release-please (push) Canceled after 0s
2026-08-16 23:51:37 -07:00
Nariman Jelveh 19cd5f4476 feat: add BytePlus ModelArk providers (chat, image, video) (#3498)
* 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.
2026-08-15 21:08:17 -07:00