diff options
27 files changed, 1504 insertions, 175 deletions
@@ -76,3 +76,4 @@ | 2025-09-09 | 300,036 (+6,695) | 229,788 (+2,715) | 529,824 (+9,410) | | 2025-09-10 | 307,287 (+7,251) | 233,435 (+3,647) | 540,722 (+10,898) | | 2025-09-11 | 314,083 (+6,796) | 237,356 (+3,921) | 551,439 (+10,717) | +| 2025-09-12 | 321,046 (+6,963) | 240,728 (+3,372) | 561,774 (+10,335) | diff --git a/cloud/app/package.json b/cloud/app/package.json index c5507fa42..d72c6dbb5 100644 --- a/cloud/app/package.json +++ b/cloud/app/package.json @@ -7,7 +7,7 @@ "dev:remote": "VITE_AUTH_URL=https://auth.dev.opencode.ai bun sst shell --stage=dev bun dev", "build": "vinxi build && ../../packages/opencode/script/schema.ts ./.output/public/config.json", "start": "vinxi start", - "version": "0.7.3" + "version": "0.7.6" }, "dependencies": { "@ibm/plex": "6.4.1", diff --git a/cloud/app/src/routes/zen/v1/chat/completions.ts b/cloud/app/src/routes/zen/v1/chat/completions.ts index dc69bb514..13a75a4a4 100644 --- a/cloud/app/src/routes/zen/v1/chat/completions.ts +++ b/cloud/app/src/routes/zen/v1/chat/completions.ts @@ -1,8 +1,26 @@ import type { APIEvent } from "@solidjs/start/server" import { handler } from "~/util/zen" +type Usage = { + prompt_tokens?: number + completion_tokens?: number + total_tokens?: number + prompt_tokens_details?: { + text_tokens?: number + audio_tokens?: number + image_tokens?: number + cached_tokens?: number + } + completion_tokens_details?: { + reasoning_tokens?: number + audio_tokens?: number + accepted_prediction_tokens?: number + rejected_prediction_tokens?: number + } +} + export function POST(input: APIEvent) { - let usage: any + let usage: Usage return handler(input, { modifyBody: (body: any) => ({ ...body, @@ -17,7 +35,7 @@ export function POST(input: APIEvent) { let json try { - json = JSON.parse(chunk.slice(6)) + json = JSON.parse(chunk.slice(6)) as { usage?: Usage } } catch (e) { return } @@ -26,11 +44,11 @@ export function POST(input: APIEvent) { usage = json.usage }, getStreamUsage: () => usage, - normalizeUsage: (usage: any) => ({ + normalizeUsage: (usage: Usage) => ({ inputTokens: usage.prompt_tokens ?? 0, outputTokens: usage.completion_tokens ?? 0, - reasoningTokens: usage.completion_tokens_details?.reasoning_tokens ?? 0, - cacheReadTokens: usage.prompt_tokens_details?.cached_tokens ?? 0, + reasoningTokens: usage.completion_tokens_details?.reasoning_tokens ?? undefined, + cacheReadTokens: usage.prompt_tokens_details?.cached_tokens ?? undefined, }), }) } diff --git a/cloud/app/src/routes/zen/v1/messages.ts b/cloud/app/src/routes/zen/v1/messages.ts index ff399b034..b2e9e275a 100644 --- a/cloud/app/src/routes/zen/v1/messages.ts +++ b/cloud/app/src/routes/zen/v1/messages.ts @@ -53,9 +53,9 @@ export function POST(input: APIEvent) { normalizeUsage: (usage: Usage) => ({ inputTokens: usage.input_tokens ?? 0, outputTokens: usage.output_tokens ?? 0, - cacheReadTokens: usage.cache_read_input_tokens ?? 0, - cacheWrite5mTokens: usage.cache_creation?.ephemeral_5m_input_tokens, - cacheWrite1hTokens: usage.cache_creation?.ephemeral_1h_input_tokens, + cacheReadTokens: usage.cache_read_input_tokens ?? undefined, + cacheWrite5mTokens: usage.cache_creation?.ephemeral_5m_input_tokens ?? undefined, + cacheWrite1hTokens: usage.cache_creation?.ephemeral_1h_input_tokens ?? undefined, }), }) } diff --git a/cloud/app/src/routes/zen/v1/responses.ts b/cloud/app/src/routes/zen/v1/responses.ts index 518a431ab..1bca91f52 100644 --- a/cloud/app/src/routes/zen/v1/responses.ts +++ b/cloud/app/src/routes/zen/v1/responses.ts @@ -1,8 +1,20 @@ import type { APIEvent } from "@solidjs/start/server" import { handler } from "~/util/zen" +type Usage = { + input_tokens?: number + input_tokens_details?: { + cached_tokens?: number + } + output_tokens?: number + output_tokens_details?: { + reasoning_tokens?: number + } + total_tokens?: number +} + export function POST(input: APIEvent) { - let usage: any + let usage: Usage return handler(input, { setAuthHeader: (headers: Headers, apiKey: string) => { headers.set("authorization", `Bearer ${apiKey}`) @@ -15,7 +27,7 @@ export function POST(input: APIEvent) { let json try { - json = JSON.parse(data.slice(6)) + json = JSON.parse(data.slice(6)) as { response?: { usage?: Usage } } } catch (e) { return } @@ -24,14 +36,14 @@ export function POST(input: APIEvent) { usage = json.response.usage }, getStreamUsage: () => usage, - normalizeUsage: (usage: any) => { + normalizeUsage: (usage: Usage) => { const inputTokens = usage.input_tokens ?? 0 const outputTokens = usage.output_tokens ?? 0 - const reasoningTokens = usage.output_tokens_details?.reasoning_tokens ?? 0 - const cacheReadTokens = usage.input_tokens_details?.cached_tokens ?? 0 + const reasoningTokens = usage.output_tokens_details?.reasoning_tokens ?? undefined + const cacheReadTokens = usage.input_tokens_details?.cached_tokens ?? undefined return { - inputTokens: inputTokens - cacheReadTokens, - outputTokens: outputTokens - reasoningTokens, + inputTokens: inputTokens - (cacheReadTokens ?? 0), + outputTokens: outputTokens - (reasoningTokens ?? 0), reasoningTokens, cacheReadTokens, } diff --git a/cloud/app/src/util/zen.ts b/cloud/app/src/util/zen.ts index 89c91c604..bff2df40e 100644 --- a/cloud/app/src/util/zen.ts +++ b/cloud/app/src/util/zen.ts @@ -10,10 +10,11 @@ import { Resource } from "@opencode/cloud-resource" type ModelCost = { input: number output: number - cacheRead: number - cacheWrite5m: number - cacheWrite1h: number + cacheRead?: number + cacheWrite5m?: number + cacheWrite1h?: number } + type Model = { id: string auth: boolean @@ -42,7 +43,7 @@ export async function handler( inputTokens: number outputTokens: number reasoningTokens?: number - cacheReadTokens: number + cacheReadTokens?: number cacheWrite5mTokens?: number cacheWrite1hTokens?: number } @@ -129,8 +130,6 @@ export async function handler( input: 0.00000125, output: 0.00001, cacheRead: 0.000000125, - cacheWrite5m: 0, - cacheWrite1h: 0, }, headerMappings: {}, providers: { @@ -147,9 +146,6 @@ export async function handler( cost: { input: 0.00000045, output: 0.0000018, - cacheRead: 0, - cacheWrite5m: 0, - cacheWrite1h: 0, }, headerMappings: {}, providers: { @@ -173,9 +169,6 @@ export async function handler( cost: { input: 0.0000006, output: 0.0000025, - cacheRead: 0, - cacheWrite5m: 0, - cacheWrite1h: 0, }, headerMappings: {}, providers: { @@ -200,8 +193,6 @@ export async function handler( input: 0, output: 0, cacheRead: 0, - cacheWrite5m: 0, - cacheWrite1h: 0, }, headerMappings: { "x-grok-conv-id": "x-opencode-session", @@ -222,9 +213,6 @@ export async function handler( cost: { input: 0.00000038, output: 0.00000153, - cacheRead: 0, - cacheWrite5m: 0, - cacheWrite1h: 0, }, headerMappings: {}, providers: { @@ -438,15 +426,30 @@ export async function handler( const inputCost = modelCost.input * inputTokens * 100 const outputCost = modelCost.output * outputTokens * 100 - const reasoningCost = reasoningTokens ? modelCost.output * reasoningTokens * 100 : undefined - const cacheReadCost = modelCost.cacheRead * cacheReadTokens * 100 - const cacheWrite5mCost = cacheWrite5mTokens ? modelCost.cacheWrite5m * cacheWrite5mTokens * 100 : undefined - const cacheWrite1hCost = cacheWrite1hTokens ? modelCost.cacheWrite1h * cacheWrite1hTokens * 100 : undefined + const reasoningCost = (() => { + if (!reasoningTokens) return undefined + return modelCost.output * reasoningTokens * 100 + })() + const cacheReadCost = (() => { + if (!cacheReadTokens) return undefined + if (!modelCost.cacheRead) return undefined + return modelCost.cacheRead * cacheReadTokens * 100 + })() + const cacheWrite5mCost = (() => { + if (!cacheWrite5mTokens) return undefined + if (!modelCost.cacheWrite5m) return undefined + return modelCost.cacheWrite5m * cacheWrite5mTokens * 100 + })() + const cacheWrite1hCost = (() => { + if (!cacheWrite1hTokens) return undefined + if (!modelCost.cacheWrite1h) return undefined + return modelCost.cacheWrite1h * cacheWrite1hTokens * 100 + })() const totalCostInCent = inputCost + outputCost + (reasoningCost ?? 0) + - cacheReadCost + + (cacheReadCost ?? 0) + (cacheWrite5mCost ?? 0) + (cacheWrite1hCost ?? 0) @@ -460,7 +463,7 @@ export async function handler( "cost.input": Math.round(inputCost), "cost.output": Math.round(outputCost), "cost.reasoning": reasoningCost ? Math.round(reasoningCost) : undefined, - "cost.cache_read": Math.round(cacheReadCost), + "cost.cache_read": cacheReadCost ? Math.round(cacheReadCost) : undefined, "cost.cache_write_5m": cacheWrite5mCost ? Math.round(cacheWrite5mCost) : undefined, "cost.cache_write_1h": cacheWrite1hCost ? Math.round(cacheWrite1hCost) : undefined, "cost.total": Math.round(totalCostInCent), @@ -480,6 +483,8 @@ export async function handler( reasoningTokens, cacheReadTokens, cacheWriteTokens: (cacheWrite5mTokens ?? 0) + (cacheWrite1hTokens ?? 0), + cacheWrite5mTokens, + cacheWrite1hTokens, cost, }) await tx diff --git a/cloud/core/migrations/0005_jazzy_skrulls.sql b/cloud/core/migrations/0005_jazzy_skrulls.sql new file mode 100644 index 000000000..774c38dd8 --- /dev/null +++ b/cloud/core/migrations/0005_jazzy_skrulls.sql @@ -0,0 +1 @@ +ALTER TABLE `usage` MODIFY COLUMN `provider` varchar(255) NOT NULL;
\ No newline at end of file diff --git a/cloud/core/migrations/0006_parallel_gauntlet.sql b/cloud/core/migrations/0006_parallel_gauntlet.sql new file mode 100644 index 000000000..a1ff78e78 --- /dev/null +++ b/cloud/core/migrations/0006_parallel_gauntlet.sql @@ -0,0 +1,2 @@ +ALTER TABLE `usage` ADD `cache_write_5m_tokens` int;--> statement-breakpoint +ALTER TABLE `usage` ADD `cache_write_1h_tokens` int;
\ No newline at end of file diff --git a/cloud/core/migrations/meta/0005_snapshot.json b/cloud/core/migrations/meta/0005_snapshot.json new file mode 100644 index 000000000..12246a6d6 --- /dev/null +++ b/cloud/core/migrations/meta/0005_snapshot.json @@ -0,0 +1,588 @@ +{ + "version": "5", + "dialect": "mysql", + "id": "d13af80e-3c70-4866-8f14-48e7ff6ff0ff", + "prevId": "06dc6226-bfbb-4ccc-b4bc-f26070c3bed5", + "tables": { + "account": { + "name": "account", + "columns": { + "id": { + "name": "id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_created": { + "name": "time_created", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "email": { + "name": "email", + "columns": ["email"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "billing": { + "name": "billing", + "columns": { + "id": { + "name": "id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_created": { + "name": "time_created", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "customer_id": { + "name": "customer_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "payment_method_id": { + "name": "payment_method_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "payment_method_last4": { + "name": "payment_method_last4", + "type": "varchar(4)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "balance": { + "name": "balance", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reload": { + "name": "reload", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "billing_workspace_id_id_pk": { + "name": "billing_workspace_id_id_pk", + "columns": ["workspace_id", "id"] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "payment": { + "name": "payment", + "columns": { + "id": { + "name": "id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_created": { + "name": "time_created", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "customer_id": { + "name": "customer_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "payment_id": { + "name": "payment_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "amount": { + "name": "amount", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "payment_workspace_id_id_pk": { + "name": "payment_workspace_id_id_pk", + "columns": ["workspace_id", "id"] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "usage": { + "name": "usage", + "columns": { + "id": { + "name": "id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_created": { + "name": "time_created", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "input_tokens": { + "name": "input_tokens", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "output_tokens": { + "name": "output_tokens", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reasoning_tokens": { + "name": "reasoning_tokens", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cache_read_tokens": { + "name": "cache_read_tokens", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cache_write_tokens": { + "name": "cache_write_tokens", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cost": { + "name": "cost", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "usage_workspace_id_id_pk": { + "name": "usage_workspace_id_id_pk", + "columns": ["workspace_id", "id"] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "key": { + "name": "key", + "columns": { + "id": { + "name": "id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_created": { + "name": "time_created", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "actor": { + "name": "actor", + "type": "json", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "old_name": { + "name": "old_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_used": { + "name": "time_used", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "global_key": { + "name": "global_key", + "columns": ["key"], + "isUnique": true + }, + "name": { + "name": "name", + "columns": ["workspace_id", "name"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "key_workspace_id_id_pk": { + "name": "key_workspace_id_id_pk", + "columns": ["workspace_id", "id"] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user": { + "name": "user", + "columns": { + "id": { + "name": "id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_created": { + "name": "time_created", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_seen": { + "name": "time_seen", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "user_email": { + "name": "user_email", + "columns": ["workspace_id", "email"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "user_workspace_id_id_pk": { + "name": "user_workspace_id_id_pk", + "columns": ["workspace_id", "id"] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "workspace": { + "name": "workspace", + "columns": { + "id": { + "name": "id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "time_created": { + "name": "time_created", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "slug": { + "name": "slug", + "columns": ["slug"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "workspace_id": { + "name": "workspace_id", + "columns": ["id"] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + } + }, + "views": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "tables": {}, + "indexes": {} + } +} diff --git a/cloud/core/migrations/meta/0006_snapshot.json b/cloud/core/migrations/meta/0006_snapshot.json new file mode 100644 index 000000000..d726b6f67 --- /dev/null +++ b/cloud/core/migrations/meta/0006_snapshot.json @@ -0,0 +1,602 @@ +{ + "version": "5", + "dialect": "mysql", + "id": "b0ad4b11-b607-46c7-8e2d-3b9823cdc5f7", + "prevId": "d13af80e-3c70-4866-8f14-48e7ff6ff0ff", + "tables": { + "account": { + "name": "account", + "columns": { + "id": { + "name": "id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_created": { + "name": "time_created", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "email": { + "name": "email", + "columns": ["email"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "billing": { + "name": "billing", + "columns": { + "id": { + "name": "id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_created": { + "name": "time_created", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "customer_id": { + "name": "customer_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "payment_method_id": { + "name": "payment_method_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "payment_method_last4": { + "name": "payment_method_last4", + "type": "varchar(4)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "balance": { + "name": "balance", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reload": { + "name": "reload", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "billing_workspace_id_id_pk": { + "name": "billing_workspace_id_id_pk", + "columns": ["workspace_id", "id"] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "payment": { + "name": "payment", + "columns": { + "id": { + "name": "id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_created": { + "name": "time_created", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "customer_id": { + "name": "customer_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "payment_id": { + "name": "payment_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "amount": { + "name": "amount", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "payment_workspace_id_id_pk": { + "name": "payment_workspace_id_id_pk", + "columns": ["workspace_id", "id"] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "usage": { + "name": "usage", + "columns": { + "id": { + "name": "id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_created": { + "name": "time_created", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "input_tokens": { + "name": "input_tokens", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "output_tokens": { + "name": "output_tokens", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reasoning_tokens": { + "name": "reasoning_tokens", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cache_read_tokens": { + "name": "cache_read_tokens", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cache_write_tokens": { + "name": "cache_write_tokens", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cache_write_5m_tokens": { + "name": "cache_write_5m_tokens", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cache_write_1h_tokens": { + "name": "cache_write_1h_tokens", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cost": { + "name": "cost", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "usage_workspace_id_id_pk": { + "name": "usage_workspace_id_id_pk", + "columns": ["workspace_id", "id"] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "key": { + "name": "key", + "columns": { + "id": { + "name": "id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_created": { + "name": "time_created", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "actor": { + "name": "actor", + "type": "json", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "old_name": { + "name": "old_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_used": { + "name": "time_used", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "global_key": { + "name": "global_key", + "columns": ["key"], + "isUnique": true + }, + "name": { + "name": "name", + "columns": ["workspace_id", "name"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "key_workspace_id_id_pk": { + "name": "key_workspace_id_id_pk", + "columns": ["workspace_id", "id"] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user": { + "name": "user", + "columns": { + "id": { + "name": "id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_created": { + "name": "time_created", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_seen": { + "name": "time_seen", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "user_email": { + "name": "user_email", + "columns": ["workspace_id", "email"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "user_workspace_id_id_pk": { + "name": "user_workspace_id_id_pk", + "columns": ["workspace_id", "id"] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "workspace": { + "name": "workspace", + "columns": { + "id": { + "name": "id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "time_created": { + "name": "time_created", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "slug": { + "name": "slug", + "columns": ["slug"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "workspace_id": { + "name": "workspace_id", + "columns": ["id"] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + } + }, + "views": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "tables": {}, + "indexes": {} + } +} diff --git a/cloud/core/migrations/meta/_journal.json b/cloud/core/migrations/meta/_journal.json index 206d5e69b..50713706e 100644 --- a/cloud/core/migrations/meta/_journal.json +++ b/cloud/core/migrations/meta/_journal.json @@ -36,6 +36,20 @@ "when": 1757627357232, "tag": "0004_first_mockingbird", "breakpoints": true + }, + { + "idx": 5, + "version": "5", + "when": 1757632304856, + "tag": "0005_jazzy_skrulls", + "breakpoints": true + }, + { + "idx": 6, + "version": "5", + "when": 1757643108507, + "tag": "0006_parallel_gauntlet", + "breakpoints": true } ] } diff --git a/cloud/core/package.json b/cloud/core/package.json index fa401cea8..932be89c1 100644 --- a/cloud/core/package.json +++ b/cloud/core/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode/cloud-core", - "version": "0.7.3", + "version": "0.7.6", "private": true, "type": "module", "dependencies": { diff --git a/cloud/core/src/schema/billing.sql.ts b/cloud/core/src/schema/billing.sql.ts index 0473df142..d94415ce9 100644 --- a/cloud/core/src/schema/billing.sql.ts +++ b/cloud/core/src/schema/billing.sql.ts @@ -1,4 +1,4 @@ -import { bigint, boolean, int, mysqlTable, varchar } from "drizzle-orm/mysql-core" +import { bigint, boolean, int, mysqlTable, varchar, json } from "drizzle-orm/mysql-core" import { timestamps, workspaceColumns } from "../drizzle/types" import { workspaceIndexes } from "./workspace.sql" @@ -34,12 +34,14 @@ export const UsageTable = mysqlTable( ...workspaceColumns, ...timestamps, model: varchar("model", { length: 255 }).notNull(), - provider: varchar("provider", { length: 255 }), + provider: varchar("provider", { length: 255 }).notNull(), inputTokens: int("input_tokens").notNull(), outputTokens: int("output_tokens").notNull(), reasoningTokens: int("reasoning_tokens"), cacheReadTokens: int("cache_read_tokens"), cacheWriteTokens: int("cache_write_tokens"), + cacheWrite5mTokens: int("cache_write_5m_tokens"), + cacheWrite1hTokens: int("cache_write_1h_tokens"), cost: bigint("cost", { mode: "number" }).notNull(), }, (table) => [...workspaceIndexes(table)], diff --git a/cloud/function/package.json b/cloud/function/package.json index 9b9728c7e..09a1be0f0 100644 --- a/cloud/function/package.json +++ b/cloud/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode/cloud-function", - "version": "0.7.3", + "version": "0.7.6", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/cloud/scripts/package.json b/cloud/scripts/package.json index fc9491148..0b8a5c5fd 100644 --- a/cloud/scripts/package.json +++ b/cloud/scripts/package.json @@ -1,12 +1,13 @@ { "name": "@opencode/cloud-scripts", - "version": "0.7.3", + "version": "0.7.6", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", "scripts": { - "start": "tsx", - "shell": "sst shell" + "shell": "sst shell -- bun tsx", + "shell-dev": "sst shell --stage dev -- bun tsx", + "shell-prod": "sst shell --stage production -- bun tsx" }, "dependencies": { "@opencode/cloud-core": "workspace:*", diff --git a/cloud/scripts/src/backfill-usage-provider.ts b/cloud/scripts/src/backfill-usage-provider.ts new file mode 100644 index 000000000..553dcb478 --- /dev/null +++ b/cloud/scripts/src/backfill-usage-provider.ts @@ -0,0 +1,10 @@ +import { Database, eq } from "@opencode/cloud-core/drizzle/index.js" +import { UsageTable } from "@opencode/cloud-core/schema/billing.sql.js" + +await Database.use(async (tx) => { + await tx + .update(UsageTable) + .set({ model: "grok-code" }) + .where(eq(UsageTable.model, "x-ai/grok-code-fast-1")) + .limit(90000) +}) diff --git a/cloud/scripts/src/placeholder.ts b/cloud/scripts/src/placeholder.ts deleted file mode 100644 index ff7bd09c0..000000000 --- a/cloud/scripts/src/placeholder.ts +++ /dev/null @@ -1 +0,0 @@ -// placeholder diff --git a/packages/function/package.json b/packages/function/package.json index 8bb1b22f5..91662a0fe 100644 --- a/packages/function/package.json +++ b/packages/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode/function", - "version": "0.7.3", + "version": "0.7.6", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/opencode/package.json b/packages/opencode/package.json index c012c7506..32319226c 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "0.7.3", + "version": "0.7.6", "name": "opencode", "type": "module", "private": true, diff --git a/packages/opencode/src/session/index.ts b/packages/opencode/src/session/index.ts index 21eaa3be8..d333828b8 100644 --- a/packages/opencode/src/session/index.ts +++ b/packages/opencode/src/session/index.ts @@ -53,6 +53,7 @@ import { defer } from "../util/defer" import { Command } from "../command" import { $ } from "bun" import { ListTool } from "../tool/ls" +import { Token } from "../util/token" export namespace Session { const log = Log.create({ service: "session" }) @@ -361,6 +362,7 @@ export namespace Session { Bus.publish(MessageV2.Event.Updated, { info: msg, }) + return msg } async function updatePart(part: MessageV2.Part) { @@ -717,14 +719,29 @@ export namespace Session { } return Provider.defaultModel() })().then((x) => Provider.getModel(x.providerID, x.modelID)) - let msgs = await messages(input.sessionID) + + let msgs = await messages(input.sessionID).then((x) => sinceSummary(x)) + + const lastAssistant = msgs.findLast((msg) => msg.info.role === "assistant") + if ( + lastAssistant?.info.role === "assistant" && + needsCompaction({ + tokens: lastAssistant.info.tokens, + model: model.info, + }) + ) { + const msg = await summarize({ + sessionID: input.sessionID, + providerID: model.providerID, + modelID: model.info.id, + }) + msgs = [msg] + } const outputLimit = Math.min(model.info.limit.output, OUTPUT_TOKEN_MAX) || OUTPUT_TOKEN_MAX using abort = lock(input.sessionID) - const lastSummary = msgs.findLast((msg) => msg.info.role === "assistant" && msg.info.summary === true) - if (lastSummary) msgs = msgs.filter((msg) => msg.info.id >= lastSummary.info.id) const numRealUserMsgs = msgs.filter( (m) => m.info.role === "user" && !m.parts.every((p) => "synthetic" in p && p.synthetic), ).length @@ -819,39 +836,21 @@ export namespace Session { const [first, ...rest] = system system = [first, rest.join("\n")] - const assistantMsg: MessageV2.Info = { - id: Identifier.ascending("message"), - role: "assistant", - system, - mode: inputAgent, - path: { - cwd: Instance.directory, - root: Instance.worktree, - }, - cost: 0, - tokens: { - input: 0, - output: 0, - reasoning: 0, - cache: { read: 0, write: 0 }, - }, - modelID: model.modelID, - providerID: model.providerID, - time: { - created: Date.now(), - }, + const processor = await createProcessor({ sessionID: input.sessionID, - } - await updateMessage(assistantMsg) + model: model.info, + providerID: model.providerID, + agent: inputAgent, + system, + }) + await using _ = defer(async () => { - if (assistantMsg.time.completed) return - await Storage.remove(["session", "message", input.sessionID, assistantMsg.id]) - await Bus.publish(MessageV2.Event.Removed, { sessionID: input.sessionID, messageID: assistantMsg.id }) + if (processor.message.time.completed) return + await Storage.remove(["session", "message", input.sessionID, processor.message.id]) + await Bus.publish(MessageV2.Event.Removed, { sessionID: input.sessionID, messageID: processor.message.id }) }) const tools: Record<string, AITool> = {} - const processor = createProcessor(assistantMsg, model.info) - const enabledTools = pipe( agent.tools, mergeDeep(await ToolRegistry.enabled(model.providerID, model.modelID, agent)), @@ -878,7 +877,7 @@ export namespace Session { const result = await item.execute(args, { sessionID: input.sessionID, abort: options.abortSignal!, - messageID: assistantMsg.id, + messageID: processor.message.id, callID: options.toolCallId, agent: agent.name, metadata: async (val) => { @@ -982,6 +981,8 @@ export namespace Session { }, }, ) + + let pointer = 0 const stream = streamText({ onError(e) { log.error("streamText error", { @@ -989,39 +990,29 @@ export namespace Session { }) }, async prepareStep({ messages, steps }) { - // Auto compact if too long - const tokens = (() => { - if (steps.length) { - const previous = steps.at(-1) - if (previous) return getUsage(model.info, previous.usage, previous.providerMetadata).tokens - } - const msg = msgs.findLast((x) => x.info.role === "assistant")?.info as MessageV2.Assistant - if (msg && msg.tokens) { - return msg.tokens - } - })() - if (tokens) { - log.info("compact check", tokens) - const count = tokens.input + tokens.cache.read + tokens.cache.write + tokens.output - if (model.info.limit.context && count > Math.max((model.info.limit.context - outputLimit) * 0.9, 0)) { - log.info("compacting in prepareStep") - const summarized = await summarize({ - sessionID: input.sessionID, - providerID: model.providerID, - modelID: model.info.id, - }) - const msgs = await Session.messages(input.sessionID).then((x) => - x.filter((x) => x.info.id >= summarized.id), - ) - return { - messages: MessageV2.toModelMessage(msgs), - } - } + const step = steps.at(-1) + if ( + step && + needsCompaction({ + tokens: getUsage(model.info, step.usage, step.providerMetadata).tokens, + model: model.info, + }) + ) { + await processor.end() + const msg = await Session.summarize({ + sessionID: input.sessionID, + providerID: model.providerID, + modelID: model.info.id, + }) + await processor.next() + pointer = messages.length - 1 + messages.push(...MessageV2.toModelMessage([msg])) } // Add queued messages to the stream const queue = (state().queued.get(input.sessionID) ?? []).filter((x) => !x.processed) if (queue.length) { + await processor.end() for (const item of queue) { if (item.processed) continue messages.push( @@ -1034,35 +1025,10 @@ export namespace Session { ) item.processed = true } - assistantMsg.time.completed = Date.now() - await updateMessage(assistantMsg) - Object.assign(assistantMsg, { - id: Identifier.ascending("message"), - role: "assistant", - system, - path: { - cwd: Instance.directory, - root: Instance.worktree, - }, - cost: 0, - tokens: { - input: 0, - output: 0, - reasoning: 0, - cache: { read: 0, write: 0 }, - }, - modelID: model.modelID, - providerID: model.providerID, - mode: inputAgent, - time: { - created: Date.now(), - }, - sessionID: input.sessionID, - }) - await updateMessage(assistantMsg) + await processor.next() } return { - messages, + messages: messages.slice(pointer), } }, async experimental_repairToolCall(input) { @@ -1150,6 +1116,7 @@ export namespace Session { item.callback(result) } state().queued.delete(input.sessionID) + Session.prune(input) return result } @@ -1421,11 +1388,60 @@ export namespace Session { }) } - function createProcessor(assistantMsg: MessageV2.Assistant, model: ModelsDev.Model) { + async function createProcessor(input: { + sessionID: string + providerID: string + model: ModelsDev.Model + system: string[] + agent: string + }) { const toolcalls: Record<string, MessageV2.ToolPart> = {} let snapshot: string | undefined let shouldStop = false - return { + + async function createMessage() { + const msg: MessageV2.Info = { + id: Identifier.ascending("message"), + role: "assistant", + system: input.system, + mode: input.agent, + path: { + cwd: Instance.directory, + root: Instance.worktree, + }, + cost: 0, + tokens: { + input: 0, + output: 0, + reasoning: 0, + cache: { read: 0, write: 0 }, + }, + modelID: input.model.id, + providerID: input.providerID, + time: { + created: Date.now(), + }, + sessionID: input.sessionID, + } + await updateMessage(msg) + return msg + } + + let assistantMsg = await createMessage() + + const result = { + async end() { + if (assistantMsg) { + assistantMsg.time.completed = Date.now() + await updateMessage(assistantMsg) + } + }, + async next() { + assistantMsg = await createMessage() + }, + get message() { + return assistantMsg + }, partFromToolCall(toolCallID: string) { return toolcalls[toolCallID] }, @@ -1581,7 +1597,7 @@ export namespace Session { break case "finish-step": - const usage = getUsage(model, value.usage, value.providerMetadata) + const usage = getUsage(input.model, value.usage, value.providerMetadata) assistantMsg.cost += usage.cost assistantMsg.tokens = usage.tokens await updatePart({ @@ -1672,7 +1688,7 @@ export namespace Session { case LoadAPIKeyError.isInstance(e): assistantMsg.error = new MessageV2.AuthError( { - providerID: model.id, + providerID: input.providerID, message: e.message, }, { cause: e }, @@ -1711,6 +1727,7 @@ export namespace Session { return { info: assistantMsg, parts: p } }, } + return result } export const RevertInput = z.object({ @@ -1784,14 +1801,7 @@ export namespace Session { draft.time.compacting = undefined }) }) - const msgs = await messages(input.sessionID) - const start = Math.max( - 0, - msgs.findLastIndex((msg) => msg.info.role === "assistant" && msg.info.summary === true), - ) - const split = start + Math.floor((msgs.length - start) / 2) - log.info("summarizing", { start, split }) - const toSummarize = msgs.slice(start, split) + const toSummarize = await messages(input.sessionID).then((x) => sinceSummary(x)) const model = await Provider.getModel(input.providerID, input.modelID) const system = [ ...SystemPrompt.summarize(model.providerID), @@ -1799,6 +1809,29 @@ export namespace Session { ...(await SystemPrompt.custom()), ] + const msg = (await updateMessage({ + id: Identifier.ascending("message"), + role: "assistant", + sessionID: input.sessionID, + system, + mode: "build", + path: { + cwd: Instance.directory, + root: Instance.worktree, + }, + cost: 0, + tokens: { + output: 0, + input: 0, + reasoning: 0, + cache: { read: 0, write: 0 }, + }, + modelID: input.modelID, + providerID: model.providerID, + time: { + created: Date.now(), + }, + })) as MessageV2.Assistant const generated = await generateText({ maxRetries: 10, model: model.language, @@ -1822,28 +1855,12 @@ export namespace Session { ], }) const usage = getUsage(model.info, generated.usage, generated.providerMetadata) - const msg: MessageV2.Info = { - id: Identifier.create("message", false, toSummarize.at(-1)!.info.time.created + 1), - role: "assistant", - sessionID: input.sessionID, - system, - mode: "build", - path: { - cwd: Instance.directory, - root: Instance.worktree, - }, - summary: true, - cost: usage.cost, - tokens: usage.tokens, - modelID: input.modelID, - providerID: model.providerID, - time: { - created: Date.now(), - completed: Date.now(), - }, - } + msg.cost += usage.cost + msg.tokens = usage.tokens + msg.summary = true + msg.time.completed = Date.now() await updateMessage(msg) - await updatePart({ + const part = await updatePart({ type: "text", sessionID: input.sessionID, messageID: msg.id, @@ -1859,7 +1876,55 @@ export namespace Session { sessionID: input.sessionID, }) - return msg + return { + info: msg, + parts: [part], + } + } + + function sinceSummary(msgs: { info: MessageV2.Info; parts: MessageV2.Part[] }[]) { + const result = [] + for (let i = msgs.length - 1; i >= 0; i--) { + const msg = msgs[i] + result.push(msg) + if (msg.info.role === "assistant" && msg.info.summary) break + } + return result.toReversed() + } + + function needsCompaction(input: { tokens: MessageV2.Assistant["tokens"]; model: ModelsDev.Model }) { + const count = input.tokens.input + input.tokens.cache.read + input.tokens.output + const output = Math.min(input.model.limit.output, OUTPUT_TOKEN_MAX) || OUTPUT_TOKEN_MAX + const usable = input.model.limit.context - output + return count > usable + } + + // goes backwards through parts until there are 40_000 tokens worth of tool + // calls. then erases output of previous tool calls. idea is to throw away old + // tool calls that are no longer relevant. + export async function prune(input: { sessionID: string }) { + const msgs = await messages(input.sessionID) + let sum = 0 + for (let msgIndex = msgs.length - 2; msgIndex >= 0; msgIndex--) { + const msg = msgs[msgIndex] + if (msg.info.role === "assistant" && msg.info.summary) return + for (let partIndex = msg.parts.length - 1; partIndex >= 0; partIndex--) { + const part = msg.parts[partIndex] + if (part.type === "tool") + if (part.state.status === "completed") { + if (part.state.time.compacted) return + sum += Token.estimate(part.state.output) + if (sum > 40_000) { + log.info("pruning", { + sum, + id: part.id, + }) + part.state.time.compacted = Date.now() + await updatePart(part) + } + } + } + } } function isLocked(sessionID: string) { diff --git a/packages/opencode/src/session/message-v2.ts b/packages/opencode/src/session/message-v2.ts index fd14afbd7..3102a611c 100644 --- a/packages/opencode/src/session/message-v2.ts +++ b/packages/opencode/src/session/message-v2.ts @@ -52,6 +52,7 @@ export namespace MessageV2 { time: z.object({ start: z.number(), end: z.number(), + compacted: z.number().optional(), }), }) .openapi({ @@ -528,7 +529,7 @@ export namespace MessageV2 { state: "output-available", toolCallId: part.callID, input: part.state.input, - output: part.state.output, + output: part.state.time.compacted ? "[Old tool result content cleared]" : part.state.output, }, ] if (part.state.status === "error") diff --git a/packages/opencode/src/util/token.ts b/packages/opencode/src/util/token.ts new file mode 100644 index 000000000..cee5adc37 --- /dev/null +++ b/packages/opencode/src/util/token.ts @@ -0,0 +1,7 @@ +export namespace Token { + const CHARS_PER_TOKEN = 4 + + export function estimate(input: string) { + return Math.max(0, Math.round((input || "").length / CHARS_PER_TOKEN)) + } +} diff --git a/packages/plugin/package.json b/packages/plugin/package.json index fad117b06..600ac8630 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/plugin", - "version": "0.7.3", + "version": "0.7.6", "type": "module", "scripts": { "typecheck": "tsc --noEmit" diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json index ff50048e0..458dca7f8 100644 --- a/packages/sdk/js/package.json +++ b/packages/sdk/js/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/sdk", - "version": "0.7.3", + "version": "0.7.6", "type": "module", "scripts": { "typecheck": "tsc --noEmit" diff --git a/packages/sdk/js/src/gen/types.gen.ts b/packages/sdk/js/src/gen/types.gen.ts index adf9d3f2c..7a2f964de 100644 --- a/packages/sdk/js/src/gen/types.gen.ts +++ b/packages/sdk/js/src/gen/types.gen.ts @@ -343,6 +343,7 @@ export type ToolStateCompleted = { time: { start: number end: number + compacted?: number } } diff --git a/packages/web/package.json b/packages/web/package.json index 7acf09a78..6f24c4082 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -1,7 +1,7 @@ { "name": "@opencode/web", "type": "module", - "version": "0.7.3", + "version": "0.7.6", "scripts": { "dev": "astro dev", "dev:remote": "sst shell --stage=dev --target=Web astro dev", diff --git a/sdks/vscode/package.json b/sdks/vscode/package.json index 1de6c560a..8273d8cc0 100644 --- a/sdks/vscode/package.json +++ b/sdks/vscode/package.json @@ -2,7 +2,7 @@ "name": "opencode", "displayName": "opencode", "description": "opencode for VS Code", - "version": "0.7.3", + "version": "0.7.6", "publisher": "sst-dev", "repository": { "type": "git", |
