Add the service layer for /api/ai/*. Everything after this is UI.
Endpoints
GET /api/ai/status → {subscriber, providers[], defaultModels{}, quota{usedToday,dailyLimit,remaining}}
POST /api/ai/suggest → {ok, feature, artifact, usage{inputTokens,outputTokens,model}, quota{}}
POST /api/ai/generate → {ok, feature, created{...}, quota{}}
Request shapes
/suggest: {feature, input, context?, model?, maxOutputTokens?}
/generate: {feature, artifact, provider?, model?, scheduleImmediately?, crossPost?}
Artifact kinds by feature
| feature |
suggest artifact kind |
generate created |
powered_template |
list |
{listId} |
powered_document |
document |
{documentId} |
message_series |
message_series |
{listId} or {scheduledMessageIds,firstScheduledAt,lastScheduledAt} with scheduleImmediately:true |
article_series |
doc_series |
{folderId, documentIds[]} |
writing_assist |
message | thread | tags |
not persistable → 422 invalid_input |
Error codes to model as first-class states
401 unauthorized, 403 subscription_required, 409 no_provider_configured, 422 invalid_input, 422 invalid_ai_output, 429 quota_exceeded, 429 rate_limited (honor Retry-After), 500/502 provider_error.
InterlinedApiException should carry the machine-readable code so the UI can branch on it instead of string-matching messages.
Acceptance criteria
Add the service layer for
/api/ai/*. Everything after this is UI.Endpoints
GET /api/ai/status→{subscriber, providers[], defaultModels{}, quota{usedToday,dailyLimit,remaining}}POST /api/ai/suggest→{ok, feature, artifact, usage{inputTokens,outputTokens,model}, quota{}}POST /api/ai/generate→{ok, feature, created{...}, quota{}}Request shapes
/suggest:{feature, input, context?, model?, maxOutputTokens?}/generate:{feature, artifact, provider?, model?, scheduleImmediately?, crossPost?}Artifact kinds by feature
kindcreatedpowered_templatelist{listId}powered_documentdocument{documentId}message_seriesmessage_series{listId}or{scheduledMessageIds,firstScheduledAt,lastScheduledAt}withscheduleImmediately:truearticle_seriesdoc_series{folderId, documentIds[]}writing_assistmessage|thread|tags422 invalid_inputError codes to model as first-class states
401 unauthorized,403 subscription_required,409 no_provider_configured,422 invalid_input,422 invalid_ai_output,429 quota_exceeded,429 rate_limited(honorRetry-After),500/502 provider_error.InterlinedApiExceptionshould carry the machine-readablecodeso the UI can branch on it instead of string-matching messages.Acceptance criteria
Services/InterlinedApiClient.Ai.cswithGetAiStatusAsync,AiSuggestAsync,AiGenerateAsync.Models/AiStatus.cs,Models/AiArtifact.cs(+ per-kind payloads),Models/AiQuota.cs,Models/AiUsage.cs.codefield from error bodies is surfaced onInterlinedApiException.GET /api/ai/statusverified live against the test account.