plans/free_model.md
Add a new Dyad-hosted "free" model for Dyad Pro users with a hard limit of 10 successful user messages per day. The limit is enforced by ../dyad-llm-engine, while the desktop app shows the user how many free-model messages remain.
This is distinct from the existing Basic Agent quota:
Dyad Free.
Dyad Free keeps the normal chat mode selector available, but in local-agent-backed modes it uses a restricted tool set similar to Basic Agent.
Dyad Free.N/10 remaining today near the model option and/or the Pro credit chip.Dyad Free is disabled or clearly marked unavailable in the picker.Dyad Free should prompt the user to switch to another model.00:00 UTC.Resets at 5:00 PM local time.../dyad-llm-engine)Introduce a first-class free model identifier, for example:
auto, name free-proPOST /v1/free/chat/completionsAvoid reusing the existing app-side auto/free OpenRouter fallback semantics. That path is for non-Pro/BYO provider behavior and would blur quota ownership.
The engine should not expose this primarily as another generic model id on /v1/chat/completions. The free model has different quota, provider-sharing, and product-policy semantics, so a dedicated route keeps the behavior explicit and avoids coupling the normal model proxy paths to free-model accounting.
Scope engine-owned Postgres tables to a dedicated schema named dyad_engine.
Use lowercase snake_case rather than dyadengine or dyadEngine; this matches
normal Postgres naming conventions and avoids quoted identifiers.
In Drizzle, define the schema with pgSchema("dyad_engine") and declare engine
tables from that schema. Existing engine tables should move into this schema as
part of the cutover to the main DB, since existing engine data does not need to
be preserved.
Add a persisted quota ledger table in the dyad_engine schema, keyed by authenticated gateway user identity:
iduser_idquota_kind = dyad_free_model_dailyquota_date = UTC date string, e.g. 2026-06-25used_countcreated_atupdated_atAdd a unique index on (user_id, quota_kind, quota_date).
Implementation should use an atomic database transaction or single upsert/update guard so parallel requests cannot exceed 10. The engine should reserve quota before opening the upstream model stream. If the upstream request fails before a model response starts, refund the reservation. If the user disconnects after generation has started, count the message.
Quota windows are UTC calendar days. Compute quota_date and resetAt from engine/server time, not client time.
The engine already calls the gateway /user/info flow in sandbox/ranker code. Reuse that pattern in a narrow helper:
userId.user_info.max_budget > 10.Do not trust client-sent user ids.
Avoid heuristic entitlement parsing based on fields like is_pro, subscription.active, or plan names for this route. The free model eligibility rule is permanent and explicit: the authenticated gateway user must have max_budget greater than 10.
Leave a code comment beside the eligibility check:
// The lowest paid Dyad Pro tier has $13.33 in monthly budget, so max_budget > 10
// includes paid Pro users while filtering out trial users.
Apply quota only on the dedicated free-model route:
POST /v1/free/chat/completions
The request body can stay OpenAI chat-completions compatible, but the route should ignore or strictly validate any client-sent model field and map server-side to the configured upstream free model.
Do not wire free-pro through /v1/responses or /v1/messages for v1 unless the app has a hard dependency on Responses/Anthropic-specific behavior. Keeping v1 chat-completions-only reduces quota/accounting surface area.
The upstream model sent from the engine to the LLM gateway is dyad/free.
This gateway model id is engine-side configuration; the desktop app only knows
about the app-facing free-pro model.
The engine must not use the end user's Dyad Pro API key for the upstream
dyad/free gateway call. For this code path:
Authorization header only to authenticate them, fetch
/user/info, derive quota identity, and check max_budget > 10.DYAD_PRO_SHARED_FREE_API_KEY as the
Authorization key when calling the LLM gateway for dyad/free.DYAD_PRO_SHARED_FREE_API_KEY
is missing.Quota counts one user-visible submitted message. Internal follow-up passes, retry continuations, todo reminders, and other same-turn local-agent mechanics must not consume additional free-model messages.
Quota error response should be machine-readable and consistent across routes:
{
"error": {
"type": "dyad_free_model_quota_exceeded",
"message": "Dyad Free has reached its daily limit.",
"limit": 10,
"remaining": 0,
"resetAt": "2026-06-26T00:00:00.000Z"
}
}
Use HTTP 429.
Add a lightweight authenticated endpoint:
GET /v1/free/quota
Response:
{
"used": 3,
"limit": 10,
"remaining": 7,
"resetAt": "2026-06-26T00:00:00.000Z"
}
The desktop app should poll/cache this like get-user-budget.
Add tests for:
429.remaining and resetAt.Add a new catalog model under the auto provider or another Dyad-owned provider row:
apiName: free-prodisplayName: Dyad Freedescription: 5 messages/day included with Dyad ProdollarSigns: 0tag: FreeUpdate both:
src/ipc/shared/remote_language_model_catalog.tssrc/components/ModelPicker.tsxThe current picker hides auto/free for Pro users. Keep that behavior for the old free model, but allow the new Pro free model.
Do not show free-pro to Dyad Pro trial users. The app can use the existing useTrialModelRestriction() / useUserBudgetInfo() signal (userBudget.isTrial) to filter the model before rendering. The engine-side max_budget > 10 eligibility check remains the source-of-truth backstop if a trial client still sends a request.
Update src/ipc/utils/get_model_client.ts / src/ipc/utils/llm_engine_provider.ts so the selected free Pro model routes to the dedicated engine endpoint POST /v1/free/chat/completions.
Important details:
enableDyadPro and the Dyad Pro API key./v1/chat/completions, /v1/responses, or /v1/messages in v1.builtinProviderId or add an explicit flag so downstream local-agent code can detect "this turn uses the free model."Add a new IPC contract rather than overloading free_agent_quota:
src/ipc/types/free_model_quota.tssrc/ipc/handlers/free_model_quota_handlers.tssrc/hooks/useFreeModelQuota.tsqueryKeys.freeModelQuota.statusThe handler calls the engine status endpoint with the Dyad Pro API key and returns:
messagesUsedmessagesLimitmessagesRemainingisQuotaExceededresetTimeKeep the existing free_agent_quota name for Basic Agent only.
Model picker:
Dyad Free to Pro users.Dyad Free to Dyad Pro trial users.2/5 remaining today in the row.Data sharing chip directly in the row, not only in the description or tooltip.Data sharing chip should have a tooltip: Data may be shared with the AI provider and used for training models.Title bar / Pro credit display:
Dyad Free: 2 of 5 messages remaining today.Chat errors:
ChatErrorBox to recognize dyad_free_model_quota_exceeded.React Query:
freeModelQuota.status after a successful Dyad Free stream.The new free model should not use tools that call other engine endpoints, such as:
web_searchweb_fetchweb_crawlgenerate_imageengineFetch(...)It may use local/read-code and MCP consent flows:
explore_codegrepread_filelist_filessearch_mcp_toolsget_mcp_tool_schemaImplementation approach:
freeModelMode?: boolean option to BuildAgentToolSetOptions.ENGINE_ENDPOINT_TOOLS = new Set(["web_search", "web_fetch", "web_crawl", "generate_image"]).shouldIncludeTool, skip those tools when freeModelMode is true.freeModelMode from handleLocalAgentStream based on selected model identity, not chat mode.Do not reuse basicAgentMode for this. Basic Agent means non-Pro plus quota; free-model mode means Pro plus selected model.
Update local-agent prompt generation to avoid advertising unavailable tools when the selected model is Dyad Free.
If no prompt text changes are needed because tool descriptions are derived only from the registered tool set, still add/adjust tests proving engine-backed tools are absent from the request snapshot.
On successful completion of a Dyad Free request:
queryKeys.freeModelQuota.status.Do not decrement quota optimistically in the app. The engine is the source of truth.
/v1/free/chat/completions, quota middleware/helper, and tests.freeModelMode tool filtering and request/prompt snapshot coverage.Dyad Free, seeing remaining count, sending a successful agent message with allowed local tools, and seeing quota-exceeded behavior at 0 remaining.Dyad app:
src/components/ModelPicker.tsxsrc/ipc/utils/get_model_client.tssrc/ipc/utils/llm_engine_provider.tssrc/ipc/shared/remote_language_model_catalog.tssrc/pro/main/ipc/handlers/local_agent/local_agent_handler.tssrc/pro/main/ipc/handlers/local_agent/tool_definitions.tssrc/components/chat/ChatErrorBox.tsxsrc/app/TitleBar.tsxsrc/lib/queryKeys.tssrc/ipc/types/free_model_quota.tssrc/ipc/handlers/free_model_quota_handlers.tssrc/hooks/useFreeModelQuota.tsEngine:
../dyad-llm-engine/src/api/free/chatCompletionsRouter.ts or similarDYAD_PRO_SHARED_FREE_API_KEY to engine env config and deployment secrets../dyad-llm-engine/src/db/schema.tspgSchema("dyad_engine")../dyad-llm-engine/src/api/freeModelQuota/ or similar../dyad-llm-engine/src/server.ts