diff options
46 files changed, 1828 insertions, 224 deletions
@@ -75,3 +75,4 @@ | 2025-09-08 | 293,341 (+4,718) | 227,073 (+1,207) | 520,414 (+5,925) | | 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) | diff --git a/cloud/app/package.json b/cloud/app/package.json index efc292be8..5ae7df59d 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.1" + "version": "0.7.2" }, "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 b68264cc4..dc69bb514 100644 --- a/cloud/app/src/routes/zen/v1/chat/completions.ts +++ b/cloud/app/src/routes/zen/v1/chat/completions.ts @@ -6,9 +6,7 @@ export function POST(input: APIEvent) { return handler(input, { modifyBody: (body: any) => ({ ...body, - stream_options: { - include_usage: true, - }, + ...(body.stream ? { stream_options: { include_usage: true } } : {}), }), setAuthHeader: (headers: Headers, apiKey: string) => { headers.set("authorization", `Bearer ${apiKey}`) diff --git a/cloud/app/src/util/zen.ts b/cloud/app/src/util/zen.ts index 9ccf9e567..d02a9c614 100644 --- a/cloud/app/src/util/zen.ts +++ b/cloud/app/src/util/zen.ts @@ -1,11 +1,35 @@ import type { APIEvent } from "@solidjs/start/server" -import { Database, eq, sql } from "@opencode/cloud-core/drizzle/index.js" +import path from "node:path" +import { and, Database, eq, isNull, sql } from "@opencode/cloud-core/drizzle/index.js" import { KeyTable } from "@opencode/cloud-core/schema/key.sql.js" import { BillingTable, UsageTable } from "@opencode/cloud-core/schema/billing.sql.js" import { centsToMicroCents } from "@opencode/cloud-core/util/price.js" import { Identifier } from "@opencode/cloud-core/identifier.js" import { Resource } from "@opencode/cloud-resource" +type ModelCost = { + input: number + output: number + cacheRead: number + cacheWrite5m: number + cacheWrite1h: number +} +type Model = { + id: string + auth: boolean + cost: ModelCost | ((usage: any) => ModelCost) + headerMappings: Record<string, string> + providers: Record< + string, + { + api: string + apiKey: string + model: string + weight?: number + } + > +} + export async function handler( input: APIEvent, opts: { @@ -28,13 +52,10 @@ export async function handler( class CreditsError extends Error {} class ModelError extends Error {} - const MODELS = { + const MODELS: Record<string, Model> = { "claude-opus-4-1": { id: "claude-opus-4-1" as const, auth: true, - api: "https://api.anthropic.com", - apiKey: Resource.ANTHROPIC_API_KEY.value, - model: "claude-opus-4-1-20250805", cost: { input: 0.000015, output: 0.000075, @@ -43,13 +64,17 @@ export async function handler( cacheWrite1h: 0.00003, }, headerMappings: {}, + providers: { + anthropic: { + api: "https://api.anthropic.com", + apiKey: Resource.ANTHROPIC_API_KEY.value, + model: "claude-opus-4-1-20250805", + }, + }, }, "claude-sonnet-4": { id: "claude-sonnet-4" as const, auth: true, - api: "https://api.anthropic.com", - apiKey: Resource.ANTHROPIC_API_KEY.value, - model: "claude-sonnet-4-20250514", cost: (usage: any) => { const totalInputTokens = usage.inputTokens + usage.cacheReadTokens + usage.cacheWrite5mTokens + usage.cacheWrite1hTokens @@ -70,13 +95,17 @@ export async function handler( } }, headerMappings: {}, + providers: { + anthropic: { + api: "https://api.anthropic.com", + apiKey: Resource.ANTHROPIC_API_KEY.value, + model: "claude-sonnet-4-20250514", + }, + }, }, "claude-3-5-haiku": { id: "claude-3-5-haiku" as const, auth: true, - api: "https://api.anthropic.com", - apiKey: Resource.ANTHROPIC_API_KEY.value, - model: "claude-3-5-haiku-20241022", cost: { input: 0.0000008, output: 0.000004, @@ -85,13 +114,17 @@ export async function handler( cacheWrite1h: 0.0000016, }, headerMappings: {}, + providers: { + anthropic: { + api: "https://api.anthropic.com", + apiKey: Resource.ANTHROPIC_API_KEY.value, + model: "claude-3-5-haiku-20241022", + }, + }, }, "gpt-5": { id: "gpt-5" as const, auth: true, - api: "https://api.openai.com", - apiKey: Resource.OPENAI_API_KEY.value, - model: "gpt-5", cost: { input: 0.00000125, output: 0.00001, @@ -100,28 +133,43 @@ export async function handler( cacheWrite1h: 0, }, headerMappings: {}, + providers: { + openai: { + api: "https://api.openai.com", + apiKey: Resource.OPENAI_API_KEY.value, + model: "gpt-5", + }, + }, }, "qwen3-coder": { id: "qwen3-coder" as const, auth: true, - api: "https://inference.baseten.co", - apiKey: Resource.BASETEN_API_KEY.value, - model: "Qwen/Qwen3-Coder-480B-A35B-Instruct", cost: { - input: 0.00000038, - output: 0.00000153, + input: 0.00000045, + output: 0.0000018, cacheRead: 0, cacheWrite5m: 0, cacheWrite1h: 0, }, headerMappings: {}, + providers: { + baseten: { + api: "https://inference.baseten.co", + apiKey: Resource.BASETEN_API_KEY.value, + model: "Qwen/Qwen3-Coder-480B-A35B-Instruct", + weight: 4, + }, + fireworks: { + api: "https://api.fireworks.ai/inference", + apiKey: Resource.FIREWORKS_API_KEY.value, + model: "accounts/fireworks/models/qwen3-coder-480b-a35b-instruct", + weight: 1, + }, + }, }, "kimi-k2": { id: "kimi-k2" as const, auth: true, - api: "https://inference.baseten.co", - apiKey: Resource.BASETEN_API_KEY.value, - model: "moonshotai/Kimi-K2-Instruct-0905", cost: { input: 0.0000006, output: 0.0000025, @@ -130,13 +178,24 @@ export async function handler( cacheWrite1h: 0, }, headerMappings: {}, + providers: { + baseten: { + api: "https://inference.baseten.co", + apiKey: Resource.BASETEN_API_KEY.value, + model: "moonshotai/Kimi-K2-Instruct-0905", + weight: 4, + }, + fireworks: { + api: "https://api.fireworks.ai/inference", + apiKey: Resource.FIREWORKS_API_KEY.value, + model: "accounts/fireworks/models/kimi-k2-instruct-0905", + weight: 1, + }, + }, }, "grok-code": { id: "grok-code" as const, auth: false, - api: "https://api.x.ai", - apiKey: Resource.XAI_API_KEY.value, - model: "grok-code", cost: { input: 0, output: 0, @@ -148,14 +207,18 @@ export async function handler( "x-grok-conv-id": "x-opencode-session", "x-grok-req-id": "x-opencode-request", }, + providers: { + xai: { + api: "https://api.x.ai", + apiKey: Resource.XAI_API_KEY.value, + model: "grok-code", + }, + }, }, // deprecated "qwen/qwen3-coder": { id: "qwen/qwen3-coder" as const, auth: true, - api: "https://inference.baseten.co", - apiKey: Resource.BASETEN_API_KEY.value, - model: "Qwen/Qwen3-Coder-480B-A35B-Instruct", cost: { input: 0.00000038, output: 0.00000153, @@ -164,6 +227,20 @@ export async function handler( cacheWrite1h: 0, }, headerMappings: {}, + providers: { + baseten: { + api: "https://inference.baseten.co", + apiKey: Resource.BASETEN_API_KEY.value, + model: "Qwen/Qwen3-Coder-480B-A35B-Instruct", + weight: 5, + }, + fireworks: { + api: "https://api.fireworks.ai/inference", + apiKey: Resource.FIREWORKS_API_KEY.value, + model: "accounts/fireworks/models/qwen3-coder-480b-a35b-instruct", + weight: 1, + }, + }, }, } @@ -195,15 +272,19 @@ export async function handler( const apiKey = await authenticate() const isFree = FREE_WORKSPACES.includes(apiKey?.workspaceID ?? "") await checkCredits() + const providerName = selectProvider() + const providerData = MODEL.providers[providerName] + logger.metric({ provider: providerName }) // Request to model provider - const res = await fetch(new URL(url.pathname.replace(/^\/zen/, "") + url.search, MODEL.api), { + const startTimestamp = Date.now() + const res = await fetch(path.posix.join(providerData.api, url.pathname.replace(/^\/zen/, "") + url.search), { method: "POST", headers: (() => { const headers = input.request.headers headers.delete("host") headers.delete("content-length") - opts.setAuthHeader(headers, MODEL.apiKey) + opts.setAuthHeader(headers, providerData.apiKey) Object.entries(MODEL.headerMappings ?? {}).forEach(([k, v]) => { headers.set(k, headers.get(v)!) }) @@ -211,7 +292,7 @@ export async function handler( })(), body: JSON.stringify({ ...(opts.modifyBody?.(body) ?? body), - model: MODEL.model, + model: providerData.model, }), }) @@ -245,7 +326,6 @@ export async function handler( const decoder = new TextDecoder() let buffer = "" let responseLength = 0 - let startTimestamp = Date.now() function pump(): Promise<void> { return ( @@ -262,7 +342,6 @@ export async function handler( logger.metric({ time_to_first_byte: Date.now() - startTimestamp }) } responseLength += value.length - buffer += decoder.decode(value, { stream: true }) const parts = buffer.split("\n\n") @@ -311,7 +390,7 @@ export async function handler( workspaceID: KeyTable.workspaceID, }) .from(KeyTable) - .where(eq(KeyTable.key, apiKey)) + .where(and(eq(KeyTable.key, apiKey), isNull(KeyTable.timeDeleted))) .then((rows) => rows[0]), ) @@ -344,6 +423,13 @@ export async function handler( if (billing.balance <= 0) throw new CreditsError("Insufficient balance") } + function selectProvider() { + const picks = Object.entries(MODEL.providers).flatMap(([name, provider]) => + Array<string>(provider.weight ?? 1).fill(name), + ) + return picks[Math.floor(Math.random() * picks.length)] + } + async function trackUsage(usage: any) { const { inputTokens, outputTokens, reasoningTokens, cacheReadTokens, cacheWrite5mTokens, cacheWrite1hTokens } = opts.normalizeUsage(usage) @@ -416,9 +502,25 @@ export async function handler( "error.message": error.message, }) + // Note: both top level "type" and "error.type" fields are used by the @ai-sdk/anthropic client to render the error message. if (error instanceof AuthError || error instanceof CreditsError || error instanceof ModelError) - return new Response(JSON.stringify({ error: { message: error.message } }), { status: 401 }) + return new Response( + JSON.stringify({ + type: "error", + error: { type: error.constructor.name, message: error.message }, + }), + { status: 401 }, + ) - return new Response(JSON.stringify({ error: { message: error.message } }), { status: 500 }) + return new Response( + JSON.stringify({ + type: "error", + error: { + type: "error", + message: error.message, + }, + }), + { status: 500 }, + ) } } diff --git a/cloud/core/migrations/0002_violet_loners.sql b/cloud/core/migrations/0002_violet_loners.sql new file mode 100644 index 000000000..17273356c --- /dev/null +++ b/cloud/core/migrations/0002_violet_loners.sql @@ -0,0 +1 @@ +ALTER TABLE `key` ADD `old_name` varchar(255);
\ No newline at end of file diff --git a/cloud/core/migrations/0003_dusty_clint_barton.sql b/cloud/core/migrations/0003_dusty_clint_barton.sql new file mode 100644 index 000000000..14efae216 --- /dev/null +++ b/cloud/core/migrations/0003_dusty_clint_barton.sql @@ -0,0 +1 @@ +ALTER TABLE `key` ADD CONSTRAINT `name` UNIQUE(`workspace_id`,`name`);
\ No newline at end of file diff --git a/cloud/core/migrations/meta/0002_snapshot.json b/cloud/core/migrations/meta/0002_snapshot.json new file mode 100644 index 000000000..45d06926c --- /dev/null +++ b/cloud/core/migrations/meta/0002_snapshot.json @@ -0,0 +1,576 @@ +{ + "version": "5", + "dialect": "mysql", + "id": "9f51ef52-31ac-4ace-8b6d-39b35efe9c81", + "prevId": "79b7ee25-1c1c-41ff-9bbf-754af257102b", + "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 + }, + "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 + } + }, + "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/0003_snapshot.json b/cloud/core/migrations/meta/0003_snapshot.json new file mode 100644 index 000000000..e832ebb00 --- /dev/null +++ b/cloud/core/migrations/meta/0003_snapshot.json @@ -0,0 +1,581 @@ +{ + "version": "5", + "dialect": "mysql", + "id": "26cebd59-f553-441c-a2b2-2f9578a0ad2b", + "prevId": "9f51ef52-31ac-4ace-8b6d-39b35efe9c81", + "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 + }, + "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/_journal.json b/cloud/core/migrations/meta/_journal.json index 131e85613..c5ea50210 100644 --- a/cloud/core/migrations/meta/_journal.json +++ b/cloud/core/migrations/meta/_journal.json @@ -15,6 +15,20 @@ "when": 1756871639102, "tag": "0001_serious_whistler", "breakpoints": true + }, + { + "idx": 2, + "version": "5", + "when": 1757597611832, + "tag": "0002_violet_loners", + "breakpoints": true + }, + { + "idx": 3, + "version": "5", + "when": 1757600397194, + "tag": "0003_dusty_clint_barton", + "breakpoints": true } ] } diff --git a/cloud/core/package.json b/cloud/core/package.json index d949a66dc..48af18043 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.1", + "version": "0.7.2", "private": true, "type": "module", "dependencies": { @@ -18,6 +18,7 @@ }, "scripts": { "db": "sst shell drizzle-kit", + "db-prod": "sst shell --stage production -- drizzle-kit", "typecheck": "tsc --noEmit" }, "devDependencies": { diff --git a/cloud/core/src/key.ts b/cloud/core/src/key.ts index b62a8961b..a8bcec401 100644 --- a/cloud/core/src/key.ts +++ b/cloud/core/src/key.ts @@ -1,7 +1,7 @@ import { z } from "zod" import { fn } from "./util/fn" import { Actor } from "./actor" -import { and, Database, eq, sql } from "./drizzle" +import { and, Database, eq, isNull, sql } from "./drizzle" import { Identifier } from "./identifier" import { KeyTable } from "./schema/key.sql" @@ -12,7 +12,7 @@ export namespace Key { tx .select() .from(KeyTable) - .where(eq(KeyTable.workspaceID, workspace)) + .where(and(eq(KeyTable.workspaceID, workspace), isNull(KeyTable.timeDeleted))) .orderBy(sql`${KeyTable.timeCreated} DESC`), ) return keys @@ -48,8 +48,24 @@ export namespace Key { export const remove = fn(z.object({ id: z.string() }), async (input) => { const workspace = Actor.workspace() - await Database.use((tx) => - tx.delete(KeyTable).where(and(eq(KeyTable.id, input.id), eq(KeyTable.workspaceID, workspace))), - ) + await Database.transaction(async (tx) => { + const row = await tx + .select({ + name: KeyTable.name, + }) + .from(KeyTable) + .where(and(eq(KeyTable.id, input.id), eq(KeyTable.workspaceID, workspace))) + .then((rows) => rows[0]) + if (!row) return + + await tx + .update(KeyTable) + .set({ + timeDeleted: sql`now()`, + oldName: row.name, + name: input.id, // Use the key ID as the name + }) + .where(and(eq(KeyTable.id, input.id), eq(KeyTable.workspaceID, workspace))) + }) }) } diff --git a/cloud/core/src/schema/key.sql.ts b/cloud/core/src/schema/key.sql.ts index 07982779b..98b99c788 100644 --- a/cloud/core/src/schema/key.sql.ts +++ b/cloud/core/src/schema/key.sql.ts @@ -10,8 +10,13 @@ export const KeyTable = mysqlTable( ...timestamps, actor: json("actor").$type<Actor.Info>(), name: varchar("name", { length: 255 }).notNull(), + oldName: varchar("old_name", { length: 255 }), key: varchar("key", { length: 255 }).notNull(), timeUsed: utc("time_used"), }, - (table) => [...workspaceIndexes(table), uniqueIndex("global_key").on(table.key)], + (table) => [ + ...workspaceIndexes(table), + uniqueIndex("global_key").on(table.key), + uniqueIndex("name").on(table.workspaceID, table.name), + ], ) diff --git a/cloud/function/package.json b/cloud/function/package.json index f90c63cf1..d6f68b6ec 100644 --- a/cloud/function/package.json +++ b/cloud/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode/cloud-function", - "version": "0.7.1", + "version": "0.7.2", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/cloud/function/sst-env.d.ts b/cloud/function/sst-env.d.ts index 11bc7adea..afa8c6fe7 100644 --- a/cloud/function/sst-env.d.ts +++ b/cloud/function/sst-env.d.ts @@ -30,6 +30,10 @@ declare module "sst" { type: "sst.sst.Linkable" username: string } + FIREWORKS_API_KEY: { + type: "sst.sst.Secret" + value: string + } GITHUB_APP_ID: { type: "sst.sst.Secret" value: string diff --git a/cloud/resource/sst-env.d.ts b/cloud/resource/sst-env.d.ts index 11bc7adea..afa8c6fe7 100644 --- a/cloud/resource/sst-env.d.ts +++ b/cloud/resource/sst-env.d.ts @@ -30,6 +30,10 @@ declare module "sst" { type: "sst.sst.Linkable" username: string } + FIREWORKS_API_KEY: { + type: "sst.sst.Secret" + value: string + } GITHUB_APP_ID: { type: "sst.sst.Secret" value: string diff --git a/cloud/scripts/package.json b/cloud/scripts/package.json index 0615843d3..c2544f55d 100644 --- a/cloud/scripts/package.json +++ b/cloud/scripts/package.json @@ -1,6 +1,6 @@ { "name": "@opencode/cloud-scripts", - "version": "0.7.1", + "version": "0.7.2", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/infra/cloud.ts b/infra/cloud.ts index d1d8d2933..9c58150c3 100644 --- a/infra/cloud.ts +++ b/infra/cloud.ts @@ -103,6 +103,7 @@ const ANTHROPIC_API_KEY = new sst.Secret("ANTHROPIC_API_KEY") const OPENAI_API_KEY = new sst.Secret("OPENAI_API_KEY") const XAI_API_KEY = new sst.Secret("XAI_API_KEY") const BASETEN_API_KEY = new sst.Secret("BASETEN_API_KEY") +const FIREWORKS_API_KEY = new sst.Secret("FIREWORKS_API_KEY") const STRIPE_SECRET_KEY = new sst.Secret("STRIPE_SECRET_KEY") const AUTH_API_URL = new sst.Linkable("AUTH_API_URL", { properties: { value: auth.url.apply((url) => url!) }, @@ -136,6 +137,7 @@ new sst.cloudflare.x.SolidStart("Console", { OPENAI_API_KEY, XAI_API_KEY, BASETEN_API_KEY, + FIREWORKS_API_KEY, ], environment: { //VITE_DOCS_URL: web.url.apply((url) => url!), diff --git a/packages/function/package.json b/packages/function/package.json index 6bfa97a64..c45c91e57 100644 --- a/packages/function/package.json +++ b/packages/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode/function", - "version": "0.7.1", + "version": "0.7.2", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/function/sst-env.d.ts b/packages/function/sst-env.d.ts index 11bc7adea..afa8c6fe7 100644 --- a/packages/function/sst-env.d.ts +++ b/packages/function/sst-env.d.ts @@ -30,6 +30,10 @@ declare module "sst" { type: "sst.sst.Linkable" username: string } + FIREWORKS_API_KEY: { + type: "sst.sst.Secret" + value: string + } GITHUB_APP_ID: { type: "sst.sst.Secret" value: string diff --git a/packages/opencode/package.json b/packages/opencode/package.json index aa2a58cd5..fc4618ddc 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.1", + "version": "0.7.2", "name": "opencode", "type": "module", "private": true, diff --git a/packages/opencode/src/cli/cmd/tui.ts b/packages/opencode/src/cli/cmd/tui.ts index 2011c26cb..86b9f31b8 100644 --- a/packages/opencode/src/cli/cmd/tui.ts +++ b/packages/opencode/src/cli/cmd/tui.ts @@ -16,6 +16,7 @@ import { Ide } from "../../ide" import { Flag } from "../../flag/flag" import { Session } from "../../session" import { Instance } from "../../project/instance" +import { $ } from "bun" declare global { const OPENCODE_TUI_PATH: string @@ -111,8 +112,7 @@ export const TuiCommand = cmd({ hostname: args.hostname, }) - let cmd = ["go", "run", "./main.go"] - let cwd = Bun.fileURLToPath(new URL("../../../../tui/cmd/opencode", import.meta.url)) + let cmd = [] as string[] const tui = Bun.embeddedFiles.find((item) => (item as File).name.includes("tui")) as File if (tui) { let binaryName = tui.name @@ -125,9 +125,13 @@ export const TuiCommand = cmd({ await Bun.write(file, tui, { mode: 0o755 }) await fs.chmod(binary, 0o755) } - cwd = process.cwd() cmd = [binary] } + if (!tui) { + const dir = Bun.fileURLToPath(new URL("../../../../tui/cmd/opencode", import.meta.url)) + await $`go build -o ./dist/tui ./main.go`.cwd(dir) + cmd = [path.join(dir, "dist/tui")] + } Log.Default.info("tui", { cmd, }) diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 786ddcc65..f4b8608d8 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -499,6 +499,7 @@ export namespace Config { .optional(), }) .optional(), + disable_paste_summary: z.boolean().optional(), }) .optional(), }) diff --git a/packages/opencode/src/id/id.ts b/packages/opencode/src/id/id.ts index 4e3ba9d48..f2b961e45 100644 --- a/packages/opencode/src/id/id.ts +++ b/packages/opencode/src/id/id.ts @@ -30,7 +30,7 @@ export namespace Identifier { function generateID(prefix: keyof typeof prefixes, descending: boolean, given?: string): string { if (!given) { - return generateNewID(prefix, descending) + return create(prefix, descending) } if (!given.startsWith(prefixes[prefix])) { @@ -49,8 +49,8 @@ export namespace Identifier { return result } - function generateNewID(prefix: keyof typeof prefixes, descending: boolean): string { - const currentTimestamp = Date.now() + export function create(prefix: keyof typeof prefixes, descending: boolean, timestamp?: number): string { + const currentTimestamp = timestamp ?? Date.now() if (currentTimestamp !== lastTimestamp) { lastTimestamp = currentTimestamp diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index dbed1f74d..834a5b2dc 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -161,7 +161,7 @@ export namespace Provider { string, { providerID: string; modelID: string; info: ModelsDev.Model; language: LanguageModel } >() - const sdk = new Map<string, SDK>() + const sdk = new Map<number, SDK>() log.info("init") @@ -326,23 +326,24 @@ export namespace Provider { providerID: provider.id, }) const s = await state() - const existing = s.sdk.get(provider.id) - if (existing) return existing const pkg = model.provider?.npm ?? provider.npm ?? provider.id + const options = { ...s.providers[provider.id]?.options } + const key = Bun.hash.xxHash32(JSON.stringify({ pkg, options })) + const existing = s.sdk.get(key) + if (existing) return existing const mod = await import(await BunProc.install(pkg, "latest")) - const fn = mod[Object.keys(mod).find((key) => key.startsWith("create"))!] - let options = { ...s.providers[provider.id]?.options } if (options["timeout"] !== undefined) { // Only override fetch if user explicitly sets timeout options["fetch"] = async (input: any, init?: any) => { return await fetch(input, { ...init, timeout: options["timeout"] }) } } + const fn = mod[Object.keys(mod).find((key) => key.startsWith("create"))!] const loaded = fn({ name: provider.id, ...options, }) - s.sdk.set(provider.id, loaded) + s.sdk.set(key, loaded) return loaded as SDK })().catch((e) => { throw new InitError({ providerID: provider.id }, { cause: e }) diff --git a/packages/opencode/src/session/index.ts b/packages/opencode/src/session/index.ts index 6b93a1275..21eaa3be8 100644 --- a/packages/opencode/src/session/index.ts +++ b/packages/opencode/src/session/index.ts @@ -86,6 +86,7 @@ export namespace Session { time: z.object({ created: z.number(), updated: z.number(), + compacting: z.number().optional(), }), revert: z .object({ @@ -137,12 +138,17 @@ export namespace Session { error: MessageV2.Assistant.shape.error, }), ), + Compacted: Bus.event( + "session.compacted", + z.object({ + sessionID: z.string(), + }), + ), } const state = Instance.state( () => { const pending = new Map<string, AbortController>() - const autoCompacting = new Map<string, boolean>() const queued = new Map< string, { @@ -156,7 +162,6 @@ export namespace Session { return { pending, - autoCompacting, queued, } }, @@ -714,24 +719,8 @@ export namespace Session { })().then((x) => Provider.getModel(x.providerID, x.modelID)) let msgs = await messages(input.sessionID) - const previous = msgs.filter((x) => x.info.role === "assistant").at(-1)?.info as MessageV2.Assistant const outputLimit = Math.min(model.info.limit.output, OUTPUT_TOKEN_MAX) || OUTPUT_TOKEN_MAX - // auto summarize if too long - if (previous && previous.tokens) { - const tokens = - previous.tokens.input + previous.tokens.cache.read + previous.tokens.cache.write + previous.tokens.output - if (model.info.limit.context && tokens > Math.max((model.info.limit.context - outputLimit) * 0.9, 0)) { - state().autoCompacting.set(input.sessionID, true) - - await summarize({ - sessionID: input.sessionID, - providerID: model.providerID, - modelID: model.info.id, - }) - return prompt(input) - } - } using abort = lock(input.sessionID) const lastSummary = msgs.findLast((msg) => msg.info.role === "assistant" && msg.info.summary === true) @@ -999,7 +988,38 @@ export namespace Session { error: e, }) }, - async prepareStep({ messages }) { + 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), + } + } + } + + // Add queued messages to the stream const queue = (state().queued.get(input.sessionID) ?? []).filter((x) => !x.processed) if (queue.length) { for (const item of queue) { @@ -1756,10 +1776,22 @@ export namespace Session { } export async function summarize(input: { sessionID: string; providerID: string; modelID: string }) { - using abort = lock(input.sessionID) + await update(input.sessionID, (draft) => { + draft.time.compacting = Date.now() + }) + await using _ = defer(async () => { + await update(input.sessionID, (draft) => { + draft.time.compacting = undefined + }) + }) const msgs = await messages(input.sessionID) - const lastSummary = msgs.findLast((msg) => msg.info.role === "assistant" && msg.info.summary === true) - const filtered = msgs.filter((msg) => !lastSummary || msg.info.id >= lastSummary.info.id) + 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 model = await Provider.getModel(input.providerID, input.modelID) const system = [ ...SystemPrompt.summarize(model.providerID), @@ -1767,36 +1799,8 @@ export namespace Session { ...(await SystemPrompt.custom()), ] - const next: MessageV2.Info = { - id: Identifier.ascending("message"), - role: "assistant", - sessionID: input.sessionID, - system, - mode: "build", - path: { - cwd: Instance.directory, - root: Instance.worktree, - }, - summary: true, - cost: 0, - modelID: input.modelID, - providerID: model.providerID, - tokens: { - input: 0, - output: 0, - reasoning: 0, - cache: { read: 0, write: 0 }, - }, - time: { - created: Date.now(), - }, - } - await updateMessage(next) - - const processor = createProcessor(next, model.info) - const stream = streamText({ + const generated = await generateText({ maxRetries: 10, - abortSignal: abort.signal, model: model.language, messages: [ ...system.map( @@ -1805,7 +1809,7 @@ export namespace Session { content: x, }), ), - ...MessageV2.toModelMessage(filtered), + ...MessageV2.toModelMessage(toSummarize), { role: "user", content: [ @@ -1817,9 +1821,45 @@ 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(), + }, + } + await updateMessage(msg) + await updatePart({ + type: "text", + sessionID: input.sessionID, + messageID: msg.id, + id: Identifier.ascending("part"), + text: generated.text, + time: { + start: Date.now(), + end: Date.now(), + }, + }) - const result = await processor.process(stream) - return result + Bus.publish(Event.Compacted, { + sessionID: input.sessionID, + }) + + return msg } function isLocked(sessionID: string) { @@ -1837,12 +1877,6 @@ export namespace Session { log.info("unlocking", { sessionID }) state().pending.delete(sessionID) - const isAutoCompacting = state().autoCompacting.get(sessionID) ?? false - if (isAutoCompacting) { - state().autoCompacting.delete(sessionID) - return - } - const session = await get(sessionID) if (session.parentID) return diff --git a/packages/opencode/src/tool/edit.ts b/packages/opencode/src/tool/edit.ts index 64a0ea167..88e453029 100644 --- a/packages/opencode/src/tool/edit.ts +++ b/packages/opencode/src/tool/edit.ts @@ -107,14 +107,12 @@ export const EditTool = Tool.define("edit", { for (const [file, issues] of Object.entries(diagnostics)) { if (issues.length === 0) continue if (file === filePath) { - output += `\nThis file has errors, please fix\n<file_diagnostics>\n${issues.map(LSP.Diagnostic.pretty).join("\n")}\n</file_diagnostics>\n` + output += `\nThis file has errors, please fix\n<file_diagnostics>\n${issues + .filter((item) => item.severity === 1) + .map(LSP.Diagnostic.pretty) + .join("\n")}\n</file_diagnostics>\n` continue } - output += `\n<project_diagnostics>\n${file}\n${issues - // TODO: may want to make more leniant for eslint - .filter((item) => item.severity === 1) - .map(LSP.Diagnostic.pretty) - .join("\n")}\n</project_diagnostics>\n` } return { @@ -599,7 +597,7 @@ export function replace(content: string, oldString: string, newString: string, r for (const replacer of [ SimpleReplacer, LineTrimmedReplacer, - BlockAnchorReplacer, + // BlockAnchorReplacer, WhitespaceNormalizedReplacer, IndentationFlexibleReplacer, EscapeNormalizedReplacer, diff --git a/packages/plugin/package.json b/packages/plugin/package.json index 03d8524d4..cdf796959 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.1", + "version": "0.7.2", "type": "module", "scripts": { "typecheck": "tsc --noEmit" diff --git a/packages/sdk/go/.release-please-manifest.json b/packages/sdk/go/.release-please-manifest.json index 64f3cdd64..76d5538a4 100644 --- a/packages/sdk/go/.release-please-manifest.json +++ b/packages/sdk/go/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.8.0" + ".": "0.9.0" } diff --git a/packages/sdk/go/.stats.yml b/packages/sdk/go/.stats.yml index 79ec5d889..db9407052 100644 --- a/packages/sdk/go/.stats.yml +++ b/packages/sdk/go/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 43 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-97b61518d8666ea7cb310af04248e00bcf8dc9753ba3c7e84471df72b3232004.yml -openapi_spec_hash: a3500531973ad999c350b87c21aa3ab8 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-46826ba8640557721614b0c9a3f1860681d825ca8d8b12869652fa25aacb0b4c.yml +openapi_spec_hash: 33b8db6fde3021579b21325ce910197d config_hash: 026ef000d34bf2f930e7b41e77d2d3ff diff --git a/packages/sdk/go/CHANGELOG.md b/packages/sdk/go/CHANGELOG.md index c0ea4a073..fad7e6837 100644 --- a/packages/sdk/go/CHANGELOG.md +++ b/packages/sdk/go/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 0.9.0 (2025-09-10) + +Full Changelog: [v0.8.0...v0.9.0](https://github.com/sst/opencode-sdk-go/compare/v0.8.0...v0.9.0) + +### Features + +- **api:** api update ([2d3a28d](https://github.com/sst/opencode-sdk-go/commit/2d3a28df5657845aa4d73087e1737d1fc8c3ce1c)) + ## 0.8.0 (2025-09-01) Full Changelog: [v0.7.0...v0.8.0](https://github.com/sst/opencode-sdk-go/compare/v0.7.0...v0.8.0) diff --git a/packages/sdk/go/README.md b/packages/sdk/go/README.md index 780111828..0fe3d32bf 100644 --- a/packages/sdk/go/README.md +++ b/packages/sdk/go/README.md @@ -24,7 +24,7 @@ Or to pin the version: <!-- x-release-please-start-version --> ```sh -go get -u 'github.com/sst/[email protected]' +go get -u 'github.com/sst/[email protected]' ``` <!-- x-release-please-end --> diff --git a/packages/sdk/go/app.go b/packages/sdk/go/app.go index 53a8aeb4a..62d86f93f 100644 --- a/packages/sdk/go/app.go +++ b/packages/sdk/go/app.go @@ -50,33 +50,37 @@ func (r *AppService) Providers(ctx context.Context, query AppProvidersParams, op } type Model struct { - ID string `json:"id,required"` - Attachment bool `json:"attachment,required"` - Cost ModelCost `json:"cost,required"` - Limit ModelLimit `json:"limit,required"` - Name string `json:"name,required"` - Options map[string]interface{} `json:"options,required"` - Reasoning bool `json:"reasoning,required"` - ReleaseDate string `json:"release_date,required"` - Temperature bool `json:"temperature,required"` - ToolCall bool `json:"tool_call,required"` - JSON modelJSON `json:"-"` + ID string `json:"id,required"` + Attachment bool `json:"attachment,required"` + Cost ModelCost `json:"cost,required"` + Limit ModelLimit `json:"limit,required"` + Name string `json:"name,required"` + Options map[string]interface{} `json:"options,required"` + Reasoning bool `json:"reasoning,required"` + ReleaseDate string `json:"release_date,required"` + Temperature bool `json:"temperature,required"` + ToolCall bool `json:"tool_call,required"` + Experimental bool `json:"experimental"` + Provider ModelProvider `json:"provider"` + JSON modelJSON `json:"-"` } // modelJSON contains the JSON metadata for the struct [Model] type modelJSON struct { - ID apijson.Field - Attachment apijson.Field - Cost apijson.Field - Limit apijson.Field - Name apijson.Field - Options apijson.Field - Reasoning apijson.Field - ReleaseDate apijson.Field - Temperature apijson.Field - ToolCall apijson.Field - raw string - ExtraFields map[string]apijson.Field + ID apijson.Field + Attachment apijson.Field + Cost apijson.Field + Limit apijson.Field + Name apijson.Field + Options apijson.Field + Reasoning apijson.Field + ReleaseDate apijson.Field + Temperature apijson.Field + ToolCall apijson.Field + Experimental apijson.Field + Provider apijson.Field + raw string + ExtraFields map[string]apijson.Field } func (r *Model) UnmarshalJSON(data []byte) (err error) { @@ -135,6 +139,26 @@ func (r modelLimitJSON) RawJSON() string { return r.raw } +type ModelProvider struct { + Npm string `json:"npm,required"` + JSON modelProviderJSON `json:"-"` +} + +// modelProviderJSON contains the JSON metadata for the struct [ModelProvider] +type modelProviderJSON struct { + Npm apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *ModelProvider) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r modelProviderJSON) RawJSON() string { + return r.raw +} + type Provider struct { ID string `json:"id,required"` Env []string `json:"env,required"` diff --git a/packages/sdk/go/config.go b/packages/sdk/go/config.go index 5469fb29e..b79bcb2ef 100644 --- a/packages/sdk/go/config.go +++ b/packages/sdk/go/config.go @@ -698,16 +698,18 @@ func (r configCommandJSON) RawJSON() string { } type ConfigExperimental struct { - Hook ConfigExperimentalHook `json:"hook"` - JSON configExperimentalJSON `json:"-"` + Hook ConfigExperimentalHook `json:"hook"` + DisablePasteSummary bool `json:"disable_paste_summary"` + JSON configExperimentalJSON `json:"-"` } // configExperimentalJSON contains the JSON metadata for the struct // [ConfigExperimental] type configExperimentalJSON struct { - Hook apijson.Field - raw string - ExtraFields map[string]apijson.Field + Hook apijson.Field + SummarizePaste apijson.Field + raw string + ExtraFields map[string]apijson.Field } func (r *ConfigExperimental) UnmarshalJSON(data []byte) (err error) { @@ -1562,34 +1564,38 @@ func (r configProviderJSON) RawJSON() string { } type ConfigProviderModel struct { - ID string `json:"id"` - Attachment bool `json:"attachment"` - Cost ConfigProviderModelsCost `json:"cost"` - Limit ConfigProviderModelsLimit `json:"limit"` - Name string `json:"name"` - Options map[string]interface{} `json:"options"` - Reasoning bool `json:"reasoning"` - ReleaseDate string `json:"release_date"` - Temperature bool `json:"temperature"` - ToolCall bool `json:"tool_call"` - JSON configProviderModelJSON `json:"-"` + ID string `json:"id"` + Attachment bool `json:"attachment"` + Cost ConfigProviderModelsCost `json:"cost"` + Experimental bool `json:"experimental"` + Limit ConfigProviderModelsLimit `json:"limit"` + Name string `json:"name"` + Options map[string]interface{} `json:"options"` + Provider ConfigProviderModelsProvider `json:"provider"` + Reasoning bool `json:"reasoning"` + ReleaseDate string `json:"release_date"` + Temperature bool `json:"temperature"` + ToolCall bool `json:"tool_call"` + JSON configProviderModelJSON `json:"-"` } // configProviderModelJSON contains the JSON metadata for the struct // [ConfigProviderModel] type configProviderModelJSON struct { - ID apijson.Field - Attachment apijson.Field - Cost apijson.Field - Limit apijson.Field - Name apijson.Field - Options apijson.Field - Reasoning apijson.Field - ReleaseDate apijson.Field - Temperature apijson.Field - ToolCall apijson.Field - raw string - ExtraFields map[string]apijson.Field + ID apijson.Field + Attachment apijson.Field + Cost apijson.Field + Experimental apijson.Field + Limit apijson.Field + Name apijson.Field + Options apijson.Field + Provider apijson.Field + Reasoning apijson.Field + ReleaseDate apijson.Field + Temperature apijson.Field + ToolCall apijson.Field + raw string + ExtraFields map[string]apijson.Field } func (r *ConfigProviderModel) UnmarshalJSON(data []byte) (err error) { @@ -1650,6 +1656,27 @@ func (r configProviderModelsLimitJSON) RawJSON() string { return r.raw } +type ConfigProviderModelsProvider struct { + Npm string `json:"npm,required"` + JSON configProviderModelsProviderJSON `json:"-"` +} + +// configProviderModelsProviderJSON contains the JSON metadata for the struct +// [ConfigProviderModelsProvider] +type configProviderModelsProviderJSON struct { + Npm apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *ConfigProviderModelsProvider) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r configProviderModelsProviderJSON) RawJSON() string { + return r.raw +} + type ConfigProviderOptions struct { APIKey string `json:"apiKey"` BaseURL string `json:"baseURL"` @@ -1726,15 +1753,15 @@ func (r ConfigShare) IsKnown() bool { // TUI specific settings type ConfigTui struct { // TUI scroll speed - ScrollSpeed float64 `json:"scroll_speed,required"` - JSON configTuiJSON `json:"-"` + ScrollSpeed float64 `json:"scroll_speed,required"` + JSON configTuiJSON `json:"-"` } // configTuiJSON contains the JSON metadata for the struct [ConfigTui] type configTuiJSON struct { - ScrollSpeed apijson.Field - raw string - ExtraFields map[string]apijson.Field + ScrollSpeed apijson.Field + raw string + ExtraFields map[string]apijson.Field } func (r *ConfigTui) UnmarshalJSON(data []byte) (err error) { diff --git a/packages/sdk/go/event.go b/packages/sdk/go/event.go index 2f44f907e..00ba202cc 100644 --- a/packages/sdk/go/event.go +++ b/packages/sdk/go/event.go @@ -63,7 +63,8 @@ type EventListResponse struct { // [EventListResponseEventSessionUpdatedProperties], // [EventListResponseEventSessionDeletedProperties], // [EventListResponseEventSessionIdleProperties], - // [EventListResponseEventSessionErrorProperties], [interface{}]. + // [EventListResponseEventSessionErrorProperties], + // [EventListResponseEventSessionCompactedProperties], [interface{}]. Properties interface{} `json:"properties,required"` Type EventListResponseType `json:"type,required"` JSON eventListResponseJSON `json:"-"` @@ -105,6 +106,7 @@ func (r *EventListResponse) UnmarshalJSON(data []byte) (err error) { // [EventListResponseEventPermissionReplied], [EventListResponseEventFileEdited], // [EventListResponseEventSessionUpdated], [EventListResponseEventSessionDeleted], // [EventListResponseEventSessionIdle], [EventListResponseEventSessionError], +// [EventListResponseEventSessionCompacted], // [EventListResponseEventServerConnected]. func (r EventListResponse) AsUnion() EventListResponseUnion { return r.union @@ -118,7 +120,8 @@ func (r EventListResponse) AsUnion() EventListResponseUnion { // [EventListResponseEventPermissionUpdated], // [EventListResponseEventPermissionReplied], [EventListResponseEventFileEdited], // [EventListResponseEventSessionUpdated], [EventListResponseEventSessionDeleted], -// [EventListResponseEventSessionIdle], [EventListResponseEventSessionError] or +// [EventListResponseEventSessionIdle], [EventListResponseEventSessionError], +// [EventListResponseEventSessionCompacted] or // [EventListResponseEventServerConnected]. type EventListResponseUnion interface { implementsEventListResponse() @@ -195,6 +198,11 @@ func init() { }, apijson.UnionVariant{ TypeFilter: gjson.JSON, + Type: reflect.TypeOf(EventListResponseEventSessionCompacted{}), + DiscriminatorValue: "session.compacted", + }, + apijson.UnionVariant{ + TypeFilter: gjson.JSON, Type: reflect.TypeOf(EventListResponseEventServerConnected{}), DiscriminatorValue: "server.connected", }, @@ -1108,6 +1116,66 @@ func (r EventListResponseEventSessionErrorType) IsKnown() bool { return false } +type EventListResponseEventSessionCompacted struct { + Properties EventListResponseEventSessionCompactedProperties `json:"properties,required"` + Type EventListResponseEventSessionCompactedType `json:"type,required"` + JSON eventListResponseEventSessionCompactedJSON `json:"-"` +} + +// eventListResponseEventSessionCompactedJSON contains the JSON metadata for the +// struct [EventListResponseEventSessionCompacted] +type eventListResponseEventSessionCompactedJSON struct { + Properties apijson.Field + Type apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *EventListResponseEventSessionCompacted) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r eventListResponseEventSessionCompactedJSON) RawJSON() string { + return r.raw +} + +func (r EventListResponseEventSessionCompacted) implementsEventListResponse() {} + +type EventListResponseEventSessionCompactedProperties struct { + SessionID string `json:"sessionID,required"` + JSON eventListResponseEventSessionCompactedPropertiesJSON `json:"-"` +} + +// eventListResponseEventSessionCompactedPropertiesJSON contains the JSON metadata +// for the struct [EventListResponseEventSessionCompactedProperties] +type eventListResponseEventSessionCompactedPropertiesJSON struct { + SessionID apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *EventListResponseEventSessionCompactedProperties) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r eventListResponseEventSessionCompactedPropertiesJSON) RawJSON() string { + return r.raw +} + +type EventListResponseEventSessionCompactedType string + +const ( + EventListResponseEventSessionCompactedTypeSessionCompacted EventListResponseEventSessionCompactedType = "session.compacted" +) + +func (r EventListResponseEventSessionCompactedType) IsKnown() bool { + switch r { + case EventListResponseEventSessionCompactedTypeSessionCompacted: + return true + } + return false +} + type EventListResponseEventServerConnected struct { Properties interface{} `json:"properties,required"` Type EventListResponseEventServerConnectedType `json:"type,required"` @@ -1163,12 +1231,13 @@ const ( EventListResponseTypeSessionDeleted EventListResponseType = "session.deleted" EventListResponseTypeSessionIdle EventListResponseType = "session.idle" EventListResponseTypeSessionError EventListResponseType = "session.error" + EventListResponseTypeSessionCompacted EventListResponseType = "session.compacted" EventListResponseTypeServerConnected EventListResponseType = "server.connected" ) func (r EventListResponseType) IsKnown() bool { switch r { - case EventListResponseTypeInstallationUpdated, EventListResponseTypeLspClientDiagnostics, EventListResponseTypeMessageUpdated, EventListResponseTypeMessageRemoved, EventListResponseTypeMessagePartUpdated, EventListResponseTypeMessagePartRemoved, EventListResponseTypePermissionUpdated, EventListResponseTypePermissionReplied, EventListResponseTypeFileEdited, EventListResponseTypeSessionUpdated, EventListResponseTypeSessionDeleted, EventListResponseTypeSessionIdle, EventListResponseTypeSessionError, EventListResponseTypeServerConnected: + case EventListResponseTypeInstallationUpdated, EventListResponseTypeLspClientDiagnostics, EventListResponseTypeMessageUpdated, EventListResponseTypeMessageRemoved, EventListResponseTypeMessagePartUpdated, EventListResponseTypeMessagePartRemoved, EventListResponseTypePermissionUpdated, EventListResponseTypePermissionReplied, EventListResponseTypeFileEdited, EventListResponseTypeSessionUpdated, EventListResponseTypeSessionDeleted, EventListResponseTypeSessionIdle, EventListResponseTypeSessionError, EventListResponseTypeSessionCompacted, EventListResponseTypeServerConnected: return true } return false diff --git a/packages/sdk/go/file.go b/packages/sdk/go/file.go index 3e1b2f42f..bc36075f1 100644 --- a/packages/sdk/go/file.go +++ b/packages/sdk/go/file.go @@ -100,15 +100,17 @@ func (r FileStatus) IsKnown() bool { } type FileNode struct { - Ignored bool `json:"ignored,required"` - Name string `json:"name,required"` - Path string `json:"path,required"` - Type FileNodeType `json:"type,required"` - JSON fileNodeJSON `json:"-"` + Absolute string `json:"absolute,required"` + Ignored bool `json:"ignored,required"` + Name string `json:"name,required"` + Path string `json:"path,required"` + Type FileNodeType `json:"type,required"` + JSON fileNodeJSON `json:"-"` } // fileNodeJSON contains the JSON metadata for the struct [FileNode] type fileNodeJSON struct { + Absolute apijson.Field Ignored apijson.Field Name apijson.Field Path apijson.Field @@ -141,16 +143,18 @@ func (r FileNodeType) IsKnown() bool { } type FileReadResponse struct { - Content string `json:"content,required"` - Type FileReadResponseType `json:"type,required"` - JSON fileReadResponseJSON `json:"-"` + Content string `json:"content,required"` + Diff string `json:"diff"` + Patch FileReadResponsePatch `json:"patch"` + JSON fileReadResponseJSON `json:"-"` } // fileReadResponseJSON contains the JSON metadata for the struct // [FileReadResponse] type fileReadResponseJSON struct { Content apijson.Field - Type apijson.Field + Diff apijson.Field + Patch apijson.Field raw string ExtraFields map[string]apijson.Field } @@ -163,19 +167,64 @@ func (r fileReadResponseJSON) RawJSON() string { return r.raw } -type FileReadResponseType string +type FileReadResponsePatch struct { + Hunks []FileReadResponsePatchHunk `json:"hunks,required"` + NewFileName string `json:"newFileName,required"` + OldFileName string `json:"oldFileName,required"` + Index string `json:"index"` + NewHeader string `json:"newHeader"` + OldHeader string `json:"oldHeader"` + JSON fileReadResponsePatchJSON `json:"-"` +} -const ( - FileReadResponseTypeRaw FileReadResponseType = "raw" - FileReadResponseTypePatch FileReadResponseType = "patch" -) +// fileReadResponsePatchJSON contains the JSON metadata for the struct +// [FileReadResponsePatch] +type fileReadResponsePatchJSON struct { + Hunks apijson.Field + NewFileName apijson.Field + OldFileName apijson.Field + Index apijson.Field + NewHeader apijson.Field + OldHeader apijson.Field + raw string + ExtraFields map[string]apijson.Field +} -func (r FileReadResponseType) IsKnown() bool { - switch r { - case FileReadResponseTypeRaw, FileReadResponseTypePatch: - return true - } - return false +func (r *FileReadResponsePatch) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r fileReadResponsePatchJSON) RawJSON() string { + return r.raw +} + +type FileReadResponsePatchHunk struct { + Lines []string `json:"lines,required"` + NewLines float64 `json:"newLines,required"` + NewStart float64 `json:"newStart,required"` + OldLines float64 `json:"oldLines,required"` + OldStart float64 `json:"oldStart,required"` + JSON fileReadResponsePatchHunkJSON `json:"-"` +} + +// fileReadResponsePatchHunkJSON contains the JSON metadata for the struct +// [FileReadResponsePatchHunk] +type fileReadResponsePatchHunkJSON struct { + Lines apijson.Field + NewLines apijson.Field + NewStart apijson.Field + OldLines apijson.Field + OldStart apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *FileReadResponsePatchHunk) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r fileReadResponsePatchHunkJSON) RawJSON() string { + return r.raw } type FileListParams struct { diff --git a/packages/sdk/go/internal/version.go b/packages/sdk/go/internal/version.go index 3c8392e9d..0e818c5b2 100644 --- a/packages/sdk/go/internal/version.go +++ b/packages/sdk/go/internal/version.go @@ -2,4 +2,4 @@ package internal -const PackageVersion = "0.8.0" // x-release-please-version +const PackageVersion = "0.9.0" // x-release-please-version diff --git a/packages/sdk/go/session.go b/packages/sdk/go/session.go index 88d71b575..6696e0faf 100644 --- a/packages/sdk/go/session.go +++ b/packages/sdk/go/session.go @@ -1332,15 +1332,17 @@ func (r sessionJSON) RawJSON() string { } type SessionTime struct { - Created float64 `json:"created,required"` - Updated float64 `json:"updated,required"` - JSON sessionTimeJSON `json:"-"` + Created float64 `json:"created,required"` + Updated float64 `json:"updated,required"` + Compacting float64 `json:"compacting"` + JSON sessionTimeJSON `json:"-"` } // sessionTimeJSON contains the JSON metadata for the struct [SessionTime] type sessionTimeJSON struct { Created apijson.Field Updated apijson.Field + Compacting apijson.Field raw string ExtraFields map[string]apijson.Field } diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json index 05f8031c3..bd36d2942 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.1", + "version": "0.7.2", "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 c753228d3..a9c52184a 100644 --- a/packages/sdk/js/src/gen/types.gen.ts +++ b/packages/sdk/js/src/gen/types.gen.ts @@ -51,6 +51,9 @@ export type Event = type: "session.error" } & EventSessionError) | ({ + type: "session.compacted" + } & EventSessionCompacted) + | ({ type: "server.connected" } & EventServerConnected) @@ -478,6 +481,7 @@ export type Session = { time: { created: number updated: number + compacting?: number } revert?: { messageID: string @@ -521,6 +525,13 @@ export type EventSessionError = { } } +export type EventSessionCompacted = { + type: "session.compacted" + properties: { + sessionID: string + } +} + export type EventServerConnected = { type: "server.connected" properties: { diff --git a/packages/tui/internal/app/app.go b/packages/tui/internal/app/app.go index 4b333a93c..4a891f282 100644 --- a/packages/tui/internal/app/app.go +++ b/packages/tui/internal/app/app.go @@ -7,6 +7,7 @@ import ( "path/filepath" "slices" "strings" + "time" "log/slog" @@ -656,6 +657,9 @@ func (a *App) IsBusy() bool { if len(a.Messages) == 0 { return false } + if a.IsCompacting() { + return true + } lastMessage := a.Messages[len(a.Messages)-1] if casted, ok := lastMessage.Info.(opencode.AssistantMessage); ok { return casted.Time.Completed == 0 @@ -663,6 +667,13 @@ func (a *App) IsBusy() bool { return false } +func (a *App) IsCompacting() bool { + if time.Since(time.UnixMilli(int64(a.Session.Time.Compacting))) < time.Second*30 { + return true + } + return false +} + func (a *App) HasAnimatingWork() bool { for _, msg := range a.Messages { switch casted := msg.Info.(type) { diff --git a/packages/tui/internal/components/chat/editor.go b/packages/tui/internal/components/chat/editor.go index 907734a12..2841e2cc8 100644 --- a/packages/tui/internal/components/chat/editor.go +++ b/packages/tui/internal/components/chat/editor.go @@ -385,6 +385,9 @@ func (m *editorComponent) Content() string { } else if m.app.IsBusy() { keyText := m.getInterruptKeyText() status := "working" + if m.app.IsCompacting() { + status = "compacting" + } if m.app.CurrentPermission.ID != "" { status = "waiting for permission" } @@ -504,7 +507,10 @@ func (m *editorComponent) Submit() (tea.Model, tea.Cmd) { commandName := strings.Split(expandedValue, " ")[0] command := m.app.Commands[commands.CommandName(commandName)] if command.Custom { - args := strings.TrimPrefix(expandedValue, command.PrimaryTrigger()+" ") + args := "" + if strings.HasPrefix(expandedValue, command.PrimaryTrigger()+" ") { + args = strings.TrimPrefix(expandedValue, command.PrimaryTrigger()+" ") + } cmds = append( cmds, util.CmdHandler(app.SendCommand{Command: string(command.Name), Args: args}), @@ -662,6 +668,11 @@ func (m *editorComponent) shouldSummarizePastedText(text string) bool { if m.app.IsBashMode { return false } + + if m.app.Config != nil && m.app.Config.Experimental.DisablePasteSummary { + return false + } + lines := strings.Split(text, "\n") lineCount := len(lines) charCount := len(text) diff --git a/packages/tui/internal/components/chat/messages.go b/packages/tui/internal/components/chat/messages.go index 053d538a4..8e36d99c1 100644 --- a/packages/tui/internal/components/chat/messages.go +++ b/packages/tui/internal/components/chat/messages.go @@ -365,6 +365,9 @@ func (m *messagesComponent) renderView() tea.Cmd { lastAssistantMessage := "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" for _, msg := range slices.Backward(m.app.Messages) { if assistant, ok := msg.Info.(opencode.AssistantMessage); ok { + if assistant.Time.Completed > 0 { + break + } lastAssistantMessage = assistant.ID break } @@ -475,6 +478,9 @@ func (m *messagesComponent) renderView() tea.Cmd { } case opencode.AssistantMessage: + if casted.Summary { + continue + } if casted.ID == m.app.Session.Revert.MessageID { reverted = true revertedMessageCount = 1 diff --git a/packages/tui/internal/tui/tui.go b/packages/tui/internal/tui/tui.go index 62a647a14..69d023315 100644 --- a/packages/tui/internal/tui/tui.go +++ b/packages/tui/internal/tui/tui.go @@ -592,10 +592,40 @@ func (a Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } if matchIndex == -1 { - a.app.Messages = append(a.app.Messages, app.Message{ + // Extract the new message ID + var newMessageID string + switch casted := msg.Properties.Info.AsUnion().(type) { + case opencode.UserMessage: + newMessageID = casted.ID + case opencode.AssistantMessage: + newMessageID = casted.ID + } + + // Find the correct insertion index by scanning backwards + // Most messages are added to the end, so start from the end + insertIndex := len(a.app.Messages) + for i := len(a.app.Messages) - 1; i >= 0; i-- { + var existingID string + switch casted := a.app.Messages[i].Info.(type) { + case opencode.UserMessage: + existingID = casted.ID + case opencode.AssistantMessage: + existingID = casted.ID + } + if existingID < newMessageID { + insertIndex = i + 1 + break + } + } + + // Create the new message + newMessage := app.Message{ Info: msg.Properties.Info.AsUnion(), Parts: []opencode.PartUnion{}, - }) + } + + // Insert at the correct position + a.app.Messages = append(a.app.Messages[:insertIndex], append([]app.Message{newMessage}, a.app.Messages[insertIndex:]...)...) } } case opencode.EventListResponseEventPermissionUpdated: @@ -627,6 +657,10 @@ func (a Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { slog.Error("Server error", "name", err.Name, "message", err.Data.Message) return a, toast.NewErrorToast(err.Data.Message, toast.WithTitle(string(err.Name))) } + case opencode.EventListResponseEventSessionCompacted: + if msg.Properties.SessionID == a.app.Session.ID { + return a, toast.NewSuccessToast("Session compacted successfully") + } case tea.WindowSizeMsg: msg.Height -= 2 // Make space for the status bar a.width, a.height = msg.Width, msg.Height diff --git a/packages/web/package.json b/packages/web/package.json index 8c1599df5..a45854c8d 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -1,7 +1,7 @@ { "name": "@opencode/web", "type": "module", - "version": "0.7.1", + "version": "0.7.2", "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 2a946f06a..ebe636348 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.1", + "version": "0.7.2", "publisher": "sst-dev", "repository": { "type": "git", diff --git a/sst-env.d.ts b/sst-env.d.ts index 7db87a85d..22caba969 100644 --- a/sst-env.d.ts +++ b/sst-env.d.ts @@ -44,6 +44,10 @@ declare module "sst" { type: "sst.sst.Linkable" username: string } + FIREWORKS_API_KEY: { + type: "sst.sst.Secret" + value: string + } GITHUB_APP_ID: { type: "sst.sst.Secret" value: string |
