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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,16 @@ jobs:
run: npm run test:e2e:long-context
- name: Verify long-context workspace billing
run: npm run test:e2e:long-context-billing
- name: Verify ten-million-token job and settlement
run: npm run test:e2e:long-context-job
- name: Save long-context verification evidence
uses: actions/upload-artifact@v4
with:
name: long-context-e2e
path: |
captures/long-context-e2e.json
captures/long-context-billing.json
captures/long-context-job.json
- name: Save classification usage evidence
uses: actions/upload-artifact@v4
with:
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -100,13 +100,16 @@ jobs:
run: npm run test:e2e:long-context
- name: Verify long-context workspace billing
run: npm run test:e2e:long-context-billing
- name: Verify ten-million-token job and settlement
run: npm run test:e2e:long-context-job
- name: Save long-context verification evidence
uses: actions/upload-artifact@v4
with:
name: long-context-e2e
path: |
captures/long-context-e2e.json
captures/long-context-billing.json
captures/long-context-job.json
- name: Save classification usage evidence
uses: actions/upload-artifact@v4
with:
Expand Down
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ the plain text (`curl classifier.dev`), the HTML and the Markdown never drift.
- Docs are plain text with UPPERCASE headings (`src/docs.ts`, `src/pages.ts`); `renderDoc` turns them into HTML and `toMarkdown` into Markdown.
- Discovery files (`/.well-known/*`, sitemap, robots, auth.md) are generated in `src/wellknown.ts` from `SITE` and the MCP tool table — edit the source, never a served file.
- The MCP servers (`src/mcp.ts`) are stateless Streamable HTTP; tools call the API through `worker.fetch` so limits and logging are shared.
- Long-context jobs in `src/long-context-job.ts` accept ordered bounded parts through a SQLite Durable Object. It holds a maximum-price workspace reservation, retains only selected evidence until completion/cancellation/24-hour expiry, and settles the original part-token total once. Keep `wrangler.example.toml`, the generated docs and the job E2E in sync with this contract.
- Never commit secrets; `.secrets.env`, `.dev.vars` are ignored. `eval/data/` is ignored except the summary copied to `src/vs-jev.json`.
- Measured numbers on the site come from `eval/`; do not type numbers in by hand.
- Jev is asked through Vercel's AI Gateway first when `AI_GATEWAY_API_KEY`
Expand Down
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -236,10 +236,20 @@ tokenization or chunking, bounding tokenizer work on pathological inputs.
No eligible evidence returns `422 long_context_no_evidence` without charge.
Explicit `model: "chunklaya"` keeps its separate legacy opt-in behavior.

For up to 10 million input tokens, funded workspaces use a long-context job:
create a UUID job with a `max_tokens` ceiling, upload ordered parts of at most
50,000 tokens and 1 MB each, then call `finish`. The server screens every part
without retaining the full source and keeps a bounded set of selected excerpts
until completion or 24-hour expiry. The final Jev call reads at most 20,000
selected evidence tokens per document. `max_tokens` reserves workspace credit;
successful completion charges the actual part-token sum at the same $0.084/M
rate. Cancellation, expiry and no eligible evidence refund the reservation.
The full API sequence and resume/status route are in the generated docs.

## Analytics

Every request writes one Analytics Engine datapoint (tier, label-set fingerprint,
country, status, count, latency). No request text is ever stored.
country, status, count, latency). No request text is written to analytics.

Long-context aggregates also go to `classifier_long_context_events`, with
context/document totals, chunk screening/selection/omission counts, phase input
Expand Down
259 changes: 259 additions & 0 deletions e2e/long-context-job.ts

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@
"test:e2e:spending": "node e2e/spending.mjs",
"test:e2e:usage": "node e2e/usage.mjs",
"test:e2e:long-context": "node e2e/long-context.mjs",
"test:e2e:long-context-billing": "bun e2e/long-context-billing.ts"
"test:e2e:long-context-billing": "bun e2e/long-context-billing.ts",
"test:e2e:long-context-job": "bun e2e/long-context-job.ts --full"
},
"keywords": [],
"author": "Michael Ryaboy",
Expand Down
45 changes: 45 additions & 0 deletions src/docs.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { vsJevText } from "./vsjev";
import { roadmapDoc } from "./newsletter";
import { INPUT_PRICE_PER_MILLION, LONG_CONTEXT_PRICING } from "./lib/classification-pricing";
import { LONG_CONTEXT_JOB_MAX_TOKENS, LONG_CONTEXT_PART_MAX_TOKENS, LONG_CONTEXT_MAX_PARTS } from "./long-context";

export const SPENDING_LIMITS = `SPENDING LIMITS

Expand Down Expand Up @@ -198,6 +199,50 @@ LONG DOCUMENTS
long_context_unavailable means processing is unavailable.


TEN-MILLION-TOKEN JOBS

Paid workspaces can classify up to ${LONG_CONTEXT_JOB_MAX_TOKENS.toLocaleString("en-US")} original input tokens as one
job. The single-request JSON API keeps its 250,000-token and 1 MB limits.
Jobs accept ordered parts so no request or Worker needs to hold the whole
document. Create a UUID yourself and reuse it if the create response is lost:

PUT /v1/long-context/jobs/{uuid}/create
{"max_tokens":${LONG_CONTEXT_JOB_MAX_TOKENS},"documents":1,"labels":["renewing","not-renewing"]}

PUT /v1/long-context/jobs/{uuid}/parts/0
{"document":0,"text":"first excerpt, including its original spacing"}

PUT /v1/long-context/jobs/{uuid}/parts/1
{"document":0,"text":"next excerpt"}

POST /v1/long-context/jobs/{uuid}/finish

Use the same Authorization: Bearer classifier_agent_... key on every call.
Each part may contain at most ${LONG_CONTEXT_PART_MAX_TOKENS.toLocaleString("en-US")} cl100k_base tokens and a 1 MB JSON body;
a job accepts at most ${LONG_CONTEXT_MAX_PARTS.toLocaleString("en-US")} parts.
Send document indexes from 0 in nondecreasing order and part numbers from 0
without gaps. Split at sentence or paragraph boundaries where possible;
concatenate the parts to recover each document exactly. Jobs allow 20
documents and 32 decisions (documents × labels in multi-label mode). They
accept labels and optional multi/instructions; dimension maps use the
single-request API. Only the fast tier is available.

max_tokens is a ceiling, not a charge: the workspace holds enough credits
for that ceiling at creation, then settles at $0.084 per million original
input tokens actually uploaded. cl100k_base counts each part separately;
the full source text is not retained. Screening runs on every part. The job
keeps only selected evidence, up to 30,000 tokens per document while open;
final Jev sees at most 20,000 evidence tokens per document. Omitted evidence
is reported in usage.long_context. Final judgment can take many requests
and minutes for a 10M-token job; poll GET .../{uuid}/status to resume.

POST .../{uuid}/cancel refunds the hold. A job expires after 24 hours;
unfinished jobs are refunded and selected evidence is deleted. Completion
deletes selected evidence, retains only the result until expiry, and charges
the exact original-part token total once. Failed final judgment can be
retried before expiry. No usable evidence returns 422 and refunds the job.


LEGACY CHUNKLAYA

Explicit model: "chunklaya" keeps the legacy opt-in service, Laya behind a
Expand Down
4 changes: 3 additions & 1 deletion src/http/account-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ import { accountMcp } from "./mcp";
export async function accountApi(request: Request, env: AppEnv & Env, ctx: ExecutionContext): Promise<Response | null> {
// Preflights carry no key and must not enter account authentication or GET-only routes.
if (request.method === "OPTIONS") {
return new URL(request.url).pathname.startsWith("/v1/account/") ? legacy.fetch(request, env, ctx) : null;
const path = new URL(request.url).pathname;
return path.startsWith("/v1/account/") || path.startsWith("/v1/long-context/jobs/")
? legacy.fetch(request, env, ctx) : null;
}
let response: Response | null;
try {
Expand Down
5 changes: 5 additions & 0 deletions src/http/classification.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ export async function accountClassification(request: Request, env: AppEnv & Part
if (!/^Bearer\s+classifier_agent_/i.test(request.headers.get("authorization") || "")) return null;
const path = new URL(request.url).pathname;
const typeSafe = path === "/v1/systemone";
const jobPath = /^\/v1\/long-context\/jobs\/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})(?:\/(create|status|finish|cancel|parts\/\d+))?$/.exec(path);
if (jobPath) {
if (!env.LONG_CONTEXT_JOBS) throw new AppError(503, "Long-context jobs are unavailable.");
return env.LONG_CONTEXT_JOBS.get(env.LONG_CONTEXT_JOBS.idFromName(jobPath[1])).fetch(request);
}
if (request.method !== "POST" || !["/", "/v1/classify", "/v1/classify/batch", "/sandbox/classify", "/v1/sandbox/classify", "/v1/systemone"].includes(path)) return null;
if (env.SPENDING_ENABLED === "true") return spendingClassification(request, env, source, ctx);
const accountId = await requireApiAccount(request, env);
Expand Down
3 changes: 2 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ export interface Env extends LayaEnv, SpendingEnv {
AI_GATEWAY_DISABLED?: string;
JEV_AE?: AnalyticsEngineDataset;
LONG_CONTEXT_AE?: AnalyticsEngineDataset;
LONG_CONTEXT_JOBS?: DurableObjectNamespace;
/** context.dev, for the chat's web search and page reads only. Never served. */
CONTEXT_API_KEY?: string;
ENTERPRISE_API_KEY?: string;
Expand Down Expand Up @@ -209,7 +210,7 @@ export function primaryModels(): string[] {

const CORS = {
"access-control-allow-origin": "*",
"access-control-allow-methods": "GET, POST, OPTIONS",
"access-control-allow-methods": "GET, POST, PUT, OPTIONS",
"access-control-allow-headers": "Content-Type, Authorization, Accept, Idempotency-Key, If-None-Match, Mcp-Session-Id, MCP-Protocol-Version, X-TypeSafe-SDK, X-TypeSafe-Runtime, X-TypeSafe-Retry-Count",
"access-control-expose-headers": "RateLimit-Limit, RateLimit-Remaining, RateLimit-Policy, Retry-After, Retry-After-Ms, x-api-version, x-typesafe-request-id, Idempotency-Key",
};
Expand Down
Loading
Loading