diff --git a/bun.lock b/bun.lock index ea4f32acfe..f9904c3374 100644 --- a/bun.lock +++ b/bun.lock @@ -226,6 +226,7 @@ "name": "@codebuff/scripts", "version": "1.0.0", "dependencies": { + "@ai-sdk/openai-compatible": "^1.0.19", "@codebuff/backend": "workspace:*", "@codebuff/bigquery": "workspace:*", "@codebuff/common": "workspace:*", @@ -381,6 +382,8 @@ "@ai-sdk/openai": ["@ai-sdk/openai@2.0.11", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.2" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-t4i+vS825EC0Gc2DdTsC5UkXIu1ScOi363noTD8DuFZp6WFPHRnW6HCyEQKxEm6cNjv3BW89rdXWqq932IFJhA=="], + "@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.19", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.10" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-hnsqPCCSNKgpZRNDOAIXZs7OcUDM4ut5ggWxj2sjB4tNL/aBn/xrM7pJkqu+WuPowyrE60wPVSlw0LvtXAlMXQ=="], + "@ai-sdk/provider": ["@ai-sdk/provider@2.0.0", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA=="], "@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.2", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.3", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-0a5a6VafkV6+0irdpqnub8WE6qzG2VMsDBpXb9NQIz8c4TG8fI+GSTFIL9sqrLEwXrHdiRj7fwJsrir4jClL0w=="], @@ -3971,6 +3974,8 @@ "@ai-sdk/gateway/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.0", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.3", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-BoQZtGcBxkeSH1zK+SRYNDtJPIPpacTeiMZqnG4Rv6xXjEwM0FH4MGs9c+PlhyEWmQCzjRM2HAotEydFhD4dYw=="], + "@ai-sdk/openai-compatible/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.10", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.5" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-T1gZ76gEIwffep6MWI0QNy9jgoybUHE7TRaHB5k54K8mF91ciGFlbtCGxDYhMH3nCRergKwYFIDeFF0hJSIQHQ=="], + "@ampproject/remapping/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.30", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q=="], "@auth/core/jose": ["jose@6.1.0", "", {}, "sha512-TTQJyoEoKcC1lscpVDCSsVgYzUDg/0Bt3WE//WiTPK6uOCQC2KZS4MpugbMWt/zyjkopgZoXhZuCi00gLudfUA=="], diff --git a/common/src/db/migrations/0030_closed_union_jack.sql b/common/src/db/migrations/0030_closed_union_jack.sql new file mode 100644 index 0000000000..cc487d73e3 --- /dev/null +++ b/common/src/db/migrations/0030_closed_union_jack.sql @@ -0,0 +1,8 @@ +ALTER TABLE "message" ALTER COLUMN "client_id" DROP NOT NULL;--> statement-breakpoint +ALTER TABLE "message" ALTER COLUMN "client_request_id" DROP NOT NULL;--> statement-breakpoint +ALTER TABLE "message" ALTER COLUMN "request" DROP NOT NULL;--> statement-breakpoint +ALTER TABLE "message" ALTER COLUMN "cache_creation_input_tokens" DROP DEFAULT;--> statement-breakpoint +ALTER TABLE "message" ALTER COLUMN "cache_creation_input_tokens" DROP NOT NULL;--> statement-breakpoint +ALTER TABLE "message" ADD COLUMN "agent_id" text;--> statement-breakpoint +ALTER TABLE "message" ADD COLUMN "reasoning_text" text;--> statement-breakpoint +ALTER TABLE "message" ADD COLUMN "reasoning_tokens" integer; \ No newline at end of file diff --git a/common/src/db/migrations/meta/0030_snapshot.json b/common/src/db/migrations/meta/0030_snapshot.json new file mode 100644 index 0000000000..bc15a5f22a --- /dev/null +++ b/common/src/db/migrations/meta/0030_snapshot.json @@ -0,0 +1,2607 @@ +{ + "id": "f6734cf1-6987-4121-b120-1f663415702d", + "prevId": "5b3ab25c-05cc-4837-977b-78c6c2240b76", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account": { + "name": "account", + "schema": "", + "columns": { + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "providerAccountId": { + "name": "providerAccountId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "token_type": { + "name": "token_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_state": { + "name": "session_state", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "account_userId_user_id_fk": { + "name": "account_userId_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "account_provider_providerAccountId_pk": { + "name": "account_provider_providerAccountId_pk", + "columns": [ + "provider", + "providerAccountId" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_config": { + "name": "agent_config", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "publisher_id": { + "name": "publisher_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "major": { + "name": "major", + "type": "integer", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "CAST(SPLIT_PART(\"agent_config\".\"version\", '.', 1) AS INTEGER)", + "type": "stored" + } + }, + "minor": { + "name": "minor", + "type": "integer", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "CAST(SPLIT_PART(\"agent_config\".\"version\", '.', 2) AS INTEGER)", + "type": "stored" + } + }, + "patch": { + "name": "patch", + "type": "integer", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "CAST(SPLIT_PART(\"agent_config\".\"version\", '.', 3) AS INTEGER)", + "type": "stored" + } + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_agent_config_publisher": { + "name": "idx_agent_config_publisher", + "columns": [ + { + "expression": "publisher_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_config_publisher_id_publisher_id_fk": { + "name": "agent_config_publisher_id_publisher_id_fk", + "tableFrom": "agent_config", + "tableTo": "publisher", + "columnsFrom": [ + "publisher_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "agent_config_publisher_id_id_version_pk": { + "name": "agent_config_publisher_id_id_version_pk", + "columns": [ + "publisher_id", + "id", + "version" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_run": { + "name": "agent_run", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "publisher_id": { + "name": "publisher_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "CASE\n WHEN agent_id ~ '^[^/@]+/[^/@]+@[^/@]+$'\n THEN split_part(agent_id, '/', 1)\n ELSE NULL\n END", + "type": "stored" + } + }, + "agent_name": { + "name": "agent_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "CASE\n WHEN agent_id ~ '^[^/@]+/[^/@]+@[^/@]+$'\n THEN split_part(split_part(agent_id, '/', 2), '@', 1)\n ELSE agent_id\n END", + "type": "stored" + } + }, + "agent_version": { + "name": "agent_version", + "type": "text", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "CASE\n WHEN agent_id ~ '^[^/@]+/[^/@]+@[^/@]+$'\n THEN split_part(agent_id, '@', 2)\n ELSE NULL\n END", + "type": "stored" + } + }, + "ancestor_run_ids": { + "name": "ancestor_run_ids", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "root_run_id": { + "name": "root_run_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "CASE WHEN array_length(ancestor_run_ids, 1) >= 1 THEN ancestor_run_ids[1] ELSE id END", + "type": "stored" + } + }, + "parent_run_id": { + "name": "parent_run_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "CASE WHEN array_length(ancestor_run_ids, 1) >= 1 THEN ancestor_run_ids[array_length(ancestor_run_ids, 1)] ELSE NULL END", + "type": "stored" + } + }, + "depth": { + "name": "depth", + "type": "integer", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "COALESCE(array_length(ancestor_run_ids, 1), 1)", + "type": "stored" + } + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "CASE WHEN completed_at IS NOT NULL THEN EXTRACT(EPOCH FROM (completed_at - created_at)) * 1000 ELSE NULL END::integer", + "type": "stored" + } + }, + "total_steps": { + "name": "total_steps", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "direct_credits": { + "name": "direct_credits", + "type": "numeric(10, 6)", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "total_credits": { + "name": "total_credits", + "type": "numeric(10, 6)", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "status": { + "name": "status", + "type": "agent_run_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_agent_run_user_id": { + "name": "idx_agent_run_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_run_parent": { + "name": "idx_agent_run_parent", + "columns": [ + { + "expression": "parent_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_run_root": { + "name": "idx_agent_run_root", + "columns": [ + { + "expression": "root_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_run_agent_id": { + "name": "idx_agent_run_agent_id", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_run_publisher": { + "name": "idx_agent_run_publisher", + "columns": [ + { + "expression": "publisher_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_run_status": { + "name": "idx_agent_run_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"agent_run\".\"status\" = 'running'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_run_ancestors_gin": { + "name": "idx_agent_run_ancestors_gin", + "columns": [ + { + "expression": "ancestor_run_ids", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "idx_agent_run_completed_publisher_agent": { + "name": "idx_agent_run_completed_publisher_agent", + "columns": [ + { + "expression": "publisher_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"agent_run\".\"status\" = 'completed'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_run_completed_recent": { + "name": "idx_agent_run_completed_recent", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "publisher_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"agent_run\".\"status\" = 'completed'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_run_completed_version": { + "name": "idx_agent_run_completed_version", + "columns": [ + { + "expression": "publisher_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_version", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"agent_run\".\"status\" = 'completed'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_run_completed_user": { + "name": "idx_agent_run_completed_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"agent_run\".\"status\" = 'completed'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_run_user_id_user_id_fk": { + "name": "agent_run_user_id_user_id_fk", + "tableFrom": "agent_run", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_step": { + "name": "agent_step", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "agent_run_id": { + "name": "agent_run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "step_number": { + "name": "step_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "CASE WHEN completed_at IS NOT NULL THEN EXTRACT(EPOCH FROM (completed_at - created_at)) * 1000 ELSE NULL END::integer", + "type": "stored" + } + }, + "credits": { + "name": "credits", + "type": "numeric(10, 6)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "child_run_ids": { + "name": "child_run_ids", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "spawned_count": { + "name": "spawned_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "array_length(child_run_ids, 1)", + "type": "stored" + } + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "agent_step_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'completed'" + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "unique_step_number_per_run": { + "name": "unique_step_number_per_run", + "columns": [ + { + "expression": "agent_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "step_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_step_run_id": { + "name": "idx_agent_step_run_id", + "columns": [ + { + "expression": "agent_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_step_children_gin": { + "name": "idx_agent_step_children_gin", + "columns": [ + { + "expression": "child_run_ids", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": { + "agent_step_agent_run_id_agent_run_id_fk": { + "name": "agent_step_agent_run_id_agent_run_id_fk", + "tableFrom": "agent_step", + "tableTo": "agent_run", + "columnsFrom": [ + "agent_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credit_ledger": { + "name": "credit_ledger", + "schema": "", + "columns": { + "operation_id": { + "name": "operation_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "principal": { + "name": "principal", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "balance": { + "name": "balance", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "grant_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_credit_ledger_active_balance": { + "name": "idx_credit_ledger_active_balance", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "balance", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "priority", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"credit_ledger\".\"balance\" != 0 AND \"credit_ledger\".\"expires_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_credit_ledger_org": { + "name": "idx_credit_ledger_org", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credit_ledger_user_id_user_id_fk": { + "name": "credit_ledger_user_id_user_id_fk", + "tableFrom": "credit_ledger", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credit_ledger_org_id_org_id_fk": { + "name": "credit_ledger_org_id_org_id_fk", + "tableFrom": "credit_ledger", + "tableTo": "org", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.encrypted_api_keys": { + "name": "encrypted_api_keys", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "api_key_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "api_key": { + "name": "api_key", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "encrypted_api_keys_user_id_user_id_fk": { + "name": "encrypted_api_keys_user_id_user_id_fk", + "tableFrom": "encrypted_api_keys", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "encrypted_api_keys_user_id_type_pk": { + "name": "encrypted_api_keys_user_id_type_pk", + "columns": [ + "user_id", + "type" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.fingerprint": { + "name": "fingerprint", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "sig_hash": { + "name": "sig_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.git_eval_results": { + "name": "git_eval_results", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "cost_mode": { + "name": "cost_mode", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reasoner_model": { + "name": "reasoner_model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agent_model": { + "name": "agent_model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "cost": { + "name": "cost", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "is_public": { + "name": "is_public", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.message": { + "name": "message", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_request_id": { + "name": "client_request_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request": { + "name": "request", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "last_message": { + "name": "last_message", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "\"message\".\"request\" -> -1", + "type": "stored" + } + }, + "reasoning_text": { + "name": "reasoning_text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "response": { + "name": "response", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "input_tokens": { + "name": "input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "cache_creation_input_tokens": { + "name": "cache_creation_input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cache_read_input_tokens": { + "name": "cache_read_input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "reasoning_tokens": { + "name": "reasoning_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "output_tokens": { + "name": "output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "cost": { + "name": "cost", + "type": "numeric(100, 20)", + "primaryKey": false, + "notNull": true + }, + "credits": { + "name": "credits", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "latency_ms": { + "name": "latency_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "repo_url": { + "name": "repo_url", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "message_user_id_idx": { + "name": "message_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "message_finished_at_user_id_idx": { + "name": "message_finished_at_user_id_idx", + "columns": [ + { + "expression": "finished_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "message_org_id_idx": { + "name": "message_org_id_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "message_org_id_finished_at_idx": { + "name": "message_org_id_finished_at_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "finished_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "message_user_id_user_id_fk": { + "name": "message_user_id_user_id_fk", + "tableFrom": "message", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "message_org_id_org_id_fk": { + "name": "message_org_id_org_id_fk", + "tableFrom": "message", + "tableTo": "org", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.org": { + "name": "org", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_subscription_id": { + "name": "stripe_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "current_period_start": { + "name": "current_period_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "current_period_end": { + "name": "current_period_end", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "auto_topup_enabled": { + "name": "auto_topup_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "auto_topup_threshold": { + "name": "auto_topup_threshold", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "auto_topup_amount": { + "name": "auto_topup_amount", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "credit_limit": { + "name": "credit_limit", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "billing_alerts": { + "name": "billing_alerts", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "usage_alerts": { + "name": "usage_alerts", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "weekly_reports": { + "name": "weekly_reports", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "org_owner_id_user_id_fk": { + "name": "org_owner_id_user_id_fk", + "tableFrom": "org", + "tableTo": "user", + "columnsFrom": [ + "owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "org_slug_unique": { + "name": "org_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + }, + "org_stripe_customer_id_unique": { + "name": "org_stripe_customer_id_unique", + "nullsNotDistinct": false, + "columns": [ + "stripe_customer_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.org_feature": { + "name": "org_feature", + "schema": "", + "columns": { + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "feature": { + "name": "feature", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_org_feature_active": { + "name": "idx_org_feature_active", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "org_feature_org_id_org_id_fk": { + "name": "org_feature_org_id_org_id_fk", + "tableFrom": "org_feature", + "tableTo": "org", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "org_feature_org_id_feature_pk": { + "name": "org_feature_org_id_feature_pk", + "columns": [ + "org_id", + "feature" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.org_invite": { + "name": "org_invite", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "org_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "accepted_at": { + "name": "accepted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "accepted_by": { + "name": "accepted_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_org_invite_token": { + "name": "idx_org_invite_token", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_org_invite_email": { + "name": "idx_org_invite_email", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_org_invite_expires": { + "name": "idx_org_invite_expires", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "org_invite_org_id_org_id_fk": { + "name": "org_invite_org_id_org_id_fk", + "tableFrom": "org_invite", + "tableTo": "org", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "org_invite_invited_by_user_id_fk": { + "name": "org_invite_invited_by_user_id_fk", + "tableFrom": "org_invite", + "tableTo": "user", + "columnsFrom": [ + "invited_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "org_invite_accepted_by_user_id_fk": { + "name": "org_invite_accepted_by_user_id_fk", + "tableFrom": "org_invite", + "tableTo": "user", + "columnsFrom": [ + "accepted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "org_invite_token_unique": { + "name": "org_invite_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.org_member": { + "name": "org_member", + "schema": "", + "columns": { + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "org_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "joined_at": { + "name": "joined_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "org_member_org_id_org_id_fk": { + "name": "org_member_org_id_org_id_fk", + "tableFrom": "org_member", + "tableTo": "org", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "org_member_user_id_user_id_fk": { + "name": "org_member_user_id_user_id_fk", + "tableFrom": "org_member", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "org_member_org_id_user_id_pk": { + "name": "org_member_org_id_user_id_pk", + "columns": [ + "org_id", + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.org_repo": { + "name": "org_repo", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "repo_url": { + "name": "repo_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "repo_name": { + "name": "repo_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "repo_owner": { + "name": "repo_owner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "approved_by": { + "name": "approved_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "approved_at": { + "name": "approved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + } + }, + "indexes": { + "idx_org_repo_active": { + "name": "idx_org_repo_active", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_org_repo_unique": { + "name": "idx_org_repo_unique", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "repo_url", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "org_repo_org_id_org_id_fk": { + "name": "org_repo_org_id_org_id_fk", + "tableFrom": "org_repo", + "tableTo": "org", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "org_repo_approved_by_user_id_fk": { + "name": "org_repo_approved_by_user_id_fk", + "tableFrom": "org_repo", + "tableTo": "user", + "columnsFrom": [ + "approved_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.publisher": { + "name": "publisher", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "verified": { + "name": "verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "bio": { + "name": "bio", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "publisher_user_id_user_id_fk": { + "name": "publisher_user_id_user_id_fk", + "tableFrom": "publisher", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "publisher_org_id_org_id_fk": { + "name": "publisher_org_id_org_id_fk", + "tableFrom": "publisher", + "tableTo": "org", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "publisher_created_by_user_id_fk": { + "name": "publisher_created_by_user_id_fk", + "tableFrom": "publisher", + "tableTo": "user", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "publisher_single_owner": { + "name": "publisher_single_owner", + "value": "(\"publisher\".\"user_id\" IS NOT NULL AND \"publisher\".\"org_id\" IS NULL) OR\n (\"publisher\".\"user_id\" IS NULL AND \"publisher\".\"org_id\" IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.referral": { + "name": "referral", + "schema": "", + "columns": { + "referrer_id": { + "name": "referrer_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "referred_id": { + "name": "referred_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "referral_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "credits": { + "name": "credits", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "referral_referrer_id_user_id_fk": { + "name": "referral_referrer_id_user_id_fk", + "tableFrom": "referral", + "tableTo": "user", + "columnsFrom": [ + "referrer_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "referral_referred_id_user_id_fk": { + "name": "referral_referred_id_user_id_fk", + "tableFrom": "referral", + "tableTo": "user", + "columnsFrom": [ + "referred_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "referral_referrer_id_referred_id_pk": { + "name": "referral_referrer_id_referred_id_pk", + "columns": [ + "referrer_id", + "referred_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "sessionToken": { + "name": "sessionToken", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires": { + "name": "expires", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "fingerprint_id": { + "name": "fingerprint_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "session_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'web'" + } + }, + "indexes": {}, + "foreignKeys": { + "session_userId_user_id_fk": { + "name": "session_userId_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_fingerprint_id_fingerprint_id_fk": { + "name": "session_fingerprint_id_fingerprint_id_fk", + "tableFrom": "session", + "tableTo": "fingerprint", + "columnsFrom": [ + "fingerprint_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sync_failure": { + "name": "sync_failure", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_attempt_at": { + "name": "last_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "retry_count": { + "name": "retry_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_sync_failure_retry": { + "name": "idx_sync_failure_retry", + "columns": [ + { + "expression": "retry_count", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"sync_failure\".\"retry_count\" < 5", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "emailVerified": { + "name": "emailVerified", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_price_id": { + "name": "stripe_price_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_quota_reset": { + "name": "next_quota_reset", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now() + INTERVAL '1 month'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "referral_code": { + "name": "referral_code", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'ref-' || gen_random_uuid()" + }, + "referral_limit": { + "name": "referral_limit", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 5 + }, + "discord_id": { + "name": "discord_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "handle": { + "name": "handle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auto_topup_enabled": { + "name": "auto_topup_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "auto_topup_threshold": { + "name": "auto_topup_threshold", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "auto_topup_amount": { + "name": "auto_topup_amount", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + }, + "user_stripe_customer_id_unique": { + "name": "user_stripe_customer_id_unique", + "nullsNotDistinct": false, + "columns": [ + "stripe_customer_id" + ] + }, + "user_referral_code_unique": { + "name": "user_referral_code_unique", + "nullsNotDistinct": false, + "columns": [ + "referral_code" + ] + }, + "user_discord_id_unique": { + "name": "user_discord_id_unique", + "nullsNotDistinct": false, + "columns": [ + "discord_id" + ] + }, + "user_handle_unique": { + "name": "user_handle_unique", + "nullsNotDistinct": false, + "columns": [ + "handle" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verificationToken": { + "name": "verificationToken", + "schema": "", + "columns": { + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires": { + "name": "expires", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "verificationToken_identifier_token_pk": { + "name": "verificationToken_identifier_token_pk", + "columns": [ + "identifier", + "token" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.referral_status": { + "name": "referral_status", + "schema": "public", + "values": [ + "pending", + "completed" + ] + }, + "public.agent_run_status": { + "name": "agent_run_status", + "schema": "public", + "values": [ + "running", + "completed", + "failed", + "cancelled" + ] + }, + "public.agent_step_status": { + "name": "agent_step_status", + "schema": "public", + "values": [ + "running", + "completed", + "skipped" + ] + }, + "public.api_key_type": { + "name": "api_key_type", + "schema": "public", + "values": [ + "anthropic", + "gemini", + "openai" + ] + }, + "public.grant_type": { + "name": "grant_type", + "schema": "public", + "values": [ + "free", + "referral", + "purchase", + "admin", + "organization" + ] + }, + "public.org_role": { + "name": "org_role", + "schema": "public", + "values": [ + "owner", + "admin", + "member" + ] + }, + "public.session_type": { + "name": "session_type", + "schema": "public", + "values": [ + "web", + "pat", + "cli" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/common/src/db/migrations/meta/_journal.json b/common/src/db/migrations/meta/_journal.json index 05e0525548..fd44a187a7 100644 --- a/common/src/db/migrations/meta/_journal.json +++ b/common/src/db/migrations/meta/_journal.json @@ -211,6 +211,13 @@ "when": 1757916820720, "tag": "0029_abandoned_azazel", "breakpoints": true + }, + { + "idx": 30, + "version": "7", + "when": 1759541867779, + "tag": "0030_closed_union_jack", + "breakpoints": true } ] } \ No newline at end of file diff --git a/common/src/db/schema.ts b/common/src/db/schema.ts index 72d42a9763..59d4dd164d 100644 --- a/common/src/db/schema.ts +++ b/common/src/db/schema.ts @@ -191,22 +191,23 @@ export const message = pgTable( { id: text('id').primaryKey(), finished_at: timestamp('finished_at', { mode: 'date' }).notNull(), - client_id: text('client_id').notNull(), - client_request_id: text('client_request_id').notNull(), + client_id: text('client_id'), + client_request_id: text('client_request_id'), model: text('model').notNull(), - request: jsonb('request').notNull(), + agent_id: text('agent_id'), + request: jsonb('request'), lastMessage: jsonb('last_message').generatedAlwaysAs( (): SQL => sql`${message.request} -> -1`, ), + reasoning_text: text('reasoning_text'), response: jsonb('response').notNull(), input_tokens: integer('input_tokens').notNull().default(0), // Always going to be 0 if using OpenRouter - cache_creation_input_tokens: integer('cache_creation_input_tokens') - .notNull() - .default(0), + cache_creation_input_tokens: integer('cache_creation_input_tokens'), cache_read_input_tokens: integer('cache_read_input_tokens') .notNull() .default(0), + reasoning_tokens: integer('reasoning_tokens'), output_tokens: integer('output_tokens').notNull(), cost: numeric('cost', { precision: 100, scale: 20 }).notNull(), credits: integer('credits').notNull(), @@ -605,7 +606,12 @@ export const agentRun = pgTable( .on(table.created_at, table.publisher_id, table.agent_name) .where(sql`${table.status} = 'completed'`), index('idx_agent_run_completed_version') - .on(table.publisher_id, table.agent_name, table.agent_version, table.created_at) + .on( + table.publisher_id, + table.agent_name, + table.agent_version, + table.created_at, + ) .where(sql`${table.status} = 'completed'`), index('idx_agent_run_completed_user') .on(table.user_id) diff --git a/packages/bigquery/src/client.ts b/packages/bigquery/src/client.ts index 851d1bbdf3..8c060649d1 100644 --- a/packages/bigquery/src/client.ts +++ b/packages/bigquery/src/client.ts @@ -1,9 +1,16 @@ +import { getErrorObject } from '@codebuff/common/util/error' import { logger } from '@codebuff/common/util/logger' import { BigQuery } from '@google-cloud/bigquery' -import { RELABELS_SCHEMA, TRACES_SCHEMA } from './schema' +import { MESSAGE_SCHEMA, RELABELS_SCHEMA, TRACES_SCHEMA } from './schema' -import type { BaseTrace, GetRelevantFilesTrace, Relabel, Trace } from './schema' +import type { + BaseTrace, + GetRelevantFilesTrace, + MessageRow, + Relabel, + Trace, +} from './schema' const DATASET = process.env.NEXT_PUBLIC_CB_ENVIRONMENT === 'prod' @@ -12,6 +19,7 @@ const DATASET = const TRACES_TABLE = 'traces' const RELABELS_TABLE = 'relabels' +const MESSAGE_TABLE = 'message' // Create a single BigQuery client instance to be used by all functions let client: BigQuery | null = null @@ -26,6 +34,9 @@ function getClient(): BigQuery { } export async function setupBigQuery(dataset: string = DATASET) { + if (client) { + return + } try { client = new BigQuery() @@ -55,6 +66,17 @@ export async function setupBigQuery(dataset: string = DATASET) { fields: ['user_id', 'agent_step_id'], }, }) + await ds.table(MESSAGE_TABLE).get({ + autoCreate: true, + schema: MESSAGE_SCHEMA, + timePartitioning: { + type: 'MONTH', + field: 'finished_at', + }, + clustering: { + fields: ['user_id'], + }, + }) } catch (error) { logger.error( { @@ -71,6 +93,34 @@ export async function setupBigQuery(dataset: string = DATASET) { } } +export async function insertMessage( + row: MessageRow, + dataset: string = DATASET, +) { + try { + await getClient() + .dataset(dataset) + .table(MESSAGE_TABLE) + .insert({ ...row, request: JSON.stringify(row.request) }) + + logger.debug( + { + ...row, + request: undefined, + }, + 'Inserted message into BigQuery', + ) + return true + } catch (error) { + logger.error( + { error: getErrorObject(error), messageId: row.id }, + 'Failed to insert message into BigQuery', + ) + + return false + } +} + export async function insertTrace(trace: Trace, dataset: string = DATASET) { try { // Create a copy of the trace and stringify payload if needed diff --git a/packages/bigquery/src/schema.ts b/packages/bigquery/src/schema.ts index 2496549902..984f420c37 100644 --- a/packages/bigquery/src/schema.ts +++ b/packages/bigquery/src/schema.ts @@ -125,3 +125,37 @@ export const RELABELS_SCHEMA: TableSchema = { { name: 'payload', type: 'JSON', mode: 'REQUIRED' }, ], } + +export type MessageRow = { + id: string + user_id: string + finished_at: Date + created_at: Date + request: unknown + reasoning_text: string + response: string + output_tokens?: number | null + reasoning_tokens?: number | null + cost?: number | null + upstream_inference_cost?: number | null + input_tokens?: number | null + cache_read_input_tokens?: number | null +} + +export const MESSAGE_SCHEMA: TableSchema = { + fields: [ + { name: 'id', type: 'STRING', mode: 'REQUIRED' }, + { name: 'user_id', type: 'STRING', mode: 'REQUIRED' }, + { name: 'finished_at', type: 'TIMESTAMP', mode: 'REQUIRED' }, + { name: 'created_at', type: 'TIMESTAMP', mode: 'REQUIRED' }, + { name: 'request', type: 'JSON', mode: 'REQUIRED' }, + { name: 'response', type: 'STRING', mode: 'REQUIRED' }, + { name: 'output_tokens', type: 'INTEGER', mode: 'NULLABLE' }, + { name: 'reasoning_text', type: 'STRING', mode: 'NULLABLE' }, + { name: 'reasoning_tokens', type: 'INTEGER', mode: 'NULLABLE' }, + { name: 'cost', type: 'FLOAT', mode: 'NULLABLE' }, + { name: 'upstream_inference_cost', type: 'FLOAT', mode: 'NULLABLE' }, + { name: 'input_tokens', type: 'INTEGER', mode: 'NULLABLE' }, + { name: 'cache_read_input_tokens', type: 'INTEGER', mode: 'NULLABLE' }, + ], +} diff --git a/packages/billing/src/balance-calculator.ts b/packages/billing/src/balance-calculator.ts index 51007ef379..f33188304a 100644 --- a/packages/billing/src/balance-calculator.ts +++ b/packages/billing/src/balance-calculator.ts @@ -1,11 +1,14 @@ import db from '@codebuff/common/db' import * as schema from '@codebuff/common/db/schema' import { withSerializableTransaction } from '@codebuff/common/db/transaction' +import { TEST_USER_ID } from '@codebuff/common/old-constants' import { GrantTypeValues } from '@codebuff/common/types/grant' +import { failure, success } from '@codebuff/common/util/error' import { logger } from '@codebuff/common/util/logger' import { and, asc, gt, isNull, or, eq, sql } from 'drizzle-orm' import type { GrantType } from '@codebuff/common/db/schema' +import type { ErrorOr } from '@codebuff/common/util/error' export interface CreditBalance { totalRemaining: number @@ -321,6 +324,116 @@ export async function consumeCredits( ) } +export async function consumeCreditsAndAddAgentStep(options: { + messageId: string + userId: string + agentId: string + clientId: string | null + clientRequestId: string | null + + startTime: Date + + model: string + reasoningText: string + response: string + + cost: number + credits: number + + inputTokens: number + cacheCreationInputTokens: number | null + cacheReadInputTokens: number + reasoningTokens: number | null + outputTokens: number +}): Promise> { + const { + messageId, + userId, + agentId, + clientId, + clientRequestId, + + startTime, + + model, + reasoningText, + response, + + cost, + credits, + + inputTokens, + cacheCreationInputTokens, + cacheReadInputTokens, + reasoningTokens, + outputTokens, + } = options + + const finishedAt = new Date() + const latencyMs = finishedAt.getTime() - startTime.getTime() + + try { + return success( + await withSerializableTransaction( + async (tx) => { + const now = new Date() + const activeGrants = await getOrderedActiveGrants(userId, now, tx) + + if (activeGrants.length === 0) { + logger.error( + { userId, credits }, + 'No active grants found to consume credits from', + ) + throw new Error('No active grants found') + } + + const result = await consumeFromOrderedGrants( + userId, + credits, + activeGrants, + tx, + ) + + if (userId === TEST_USER_ID) { + return { ...result, agentStepId: 'test-step-id' } + } + const stepId = crypto.randomUUID() + + try { + await tx.insert(schema.message).values({ + id: messageId, + agent_id: agentId, + finished_at: new Date(), + client_id: clientId, + client_request_id: clientRequestId, + model, + reasoning_text: reasoningText, + response, + input_tokens: inputTokens, + cache_creation_input_tokens: cacheCreationInputTokens, + cache_read_input_tokens: cacheReadInputTokens, + reasoning_tokens: reasoningTokens, + output_tokens: outputTokens, + cost: cost.toString(), + credits, + latency_ms: latencyMs, + user_id: userId, + }) + } catch (error) { + logger.error({ ...options, error }, 'Failed to add message') + throw error + } + + return { ...result, agentStepId: stepId } + }, + { userId, credits }, + ), + ) + } catch (error) { + return failure(error) + } +} + /** * Calculate the total credits used during the current billing cycle for a user * by summing the difference between initial and remaining amounts for all relevant grants. diff --git a/scripts/fat-sdk-openrouter-example.ts b/scripts/fat-sdk-openrouter-example.ts new file mode 100644 index 0000000000..c080ab430f --- /dev/null +++ b/scripts/fat-sdk-openrouter-example.ts @@ -0,0 +1,49 @@ +import { createOpenAICompatible } from '@ai-sdk/openai-compatible' +import { websiteUrl } from '@codebuff/npm-app/config' +import { streamText } from 'ai' + +const codebuffBackendProvider = createOpenAICompatible({ + name: 'codebuff', + apiKey: '12345', + baseURL: websiteUrl + '/api/v1', +}) + +const response = streamText({ + model: codebuffBackendProvider('anthropic/claude-sonnet-4.5'), + messages: [ + { + role: 'user', + content: [ + { + type: 'text', + text: 'This is a bunch of text just to fill out some space. Ignore this.'.repeat( + 100, + ), + }, + { + type: 'text', + text: 'Hello', + providerOptions: { + openaiCompatible: { + cache_control: { type: 'ephemeral' }, + }, + }, + }, + ], + }, + ], + providerOptions: { + codebuff: { + // all these get directly added to the body at the top level + reasoningEffort: 'low', + codebuff_metadata: { + agent_run_id: '19b636d9-bfbf-40ff-b3e9-92dc86f4a8d0', + client_id: 'test-client-id-123', + client_request_id: 'test-client-session-id-456', + }, + }, + }, +}) +for await (const chunk of response.fullStream) { + console.log('asdf', { chunk }) +} diff --git a/scripts/package.json b/scripts/package.json index c4004dc9aa..ebdee21058 100644 --- a/scripts/package.json +++ b/scripts/package.json @@ -21,8 +21,9 @@ "bun": ">=1.2.11" }, "dependencies": { - "@codebuff/bigquery": "workspace:*", + "@ai-sdk/openai-compatible": "^1.0.19", "@codebuff/backend": "workspace:*", + "@codebuff/bigquery": "workspace:*", "@codebuff/common": "workspace:*", "lodash": "^4.17.21" }, diff --git a/web/src/app/admin/traces/utils/trace-processing.ts b/web/src/app/admin/traces/utils/trace-processing.ts index 0f82435625..1e5f16c0ad 100644 --- a/web/src/app/admin/traces/utils/trace-processing.ts +++ b/web/src/app/admin/traces/utils/trace-processing.ts @@ -147,10 +147,10 @@ export function buildTimelineFromMessages( // Group messages by client_request_id const messagesByRequestId = messages.reduce( (acc, msg) => { - if (!acc[msg.client_request_id]) { - acc[msg.client_request_id] = [] + if (!acc[msg.client_request_id ?? 'NULL']) { + acc[msg.client_request_id ?? 'NULL'] = [] } - acc[msg.client_request_id].push(msg) + acc[msg.client_request_id ?? 'NULL'].push(msg) return acc }, {} as Record diff --git a/web/src/app/api/admin/traces/[clientRequestId]/messages/route.ts b/web/src/app/api/admin/traces/[clientRequestId]/messages/route.ts index d7e5b35b78..c00ea14758 100644 --- a/web/src/app/api/admin/traces/[clientRequestId]/messages/route.ts +++ b/web/src/app/api/admin/traces/[clientRequestId]/messages/route.ts @@ -16,7 +16,7 @@ interface RouteParams { export interface TraceMessage { id: string - client_request_id: string + client_request_id: string | null user_id: string | null model: string request: any diff --git a/web/src/app/api/admin/traces/[clientRequestId]/timeline/route.ts b/web/src/app/api/admin/traces/[clientRequestId]/timeline/route.ts index 42d3b33a5c..29e8fc2cdc 100644 --- a/web/src/app/api/admin/traces/[clientRequestId]/timeline/route.ts +++ b/web/src/app/api/admin/traces/[clientRequestId]/timeline/route.ts @@ -70,7 +70,7 @@ export async function GET(req: NextRequest, { params }: RouteParams) { const allMessages = await db .select() .from(schema.message) - .where(eq(schema.message.client_id, clientId)) + .where(eq(schema.message.client_id, clientId ?? 'NULL')) .orderBy(schema.message.finished_at) // Build timeline events from messages using utility function diff --git a/web/src/app/api/admin/traces/client/[clientId]/sessions/route.ts b/web/src/app/api/admin/traces/client/[clientId]/sessions/route.ts index 0efc245865..b637c49fe8 100644 --- a/web/src/app/api/admin/traces/client/[clientId]/sessions/route.ts +++ b/web/src/app/api/admin/traces/client/[clientId]/sessions/route.ts @@ -80,7 +80,7 @@ export async function GET(req: NextRequest, { params }: RouteParams) { // Transform messages into client messages const clientMessages: ClientMessage[] = messages.map((msg) => ({ id: msg.id, - client_request_id: msg.client_request_id, + client_request_id: msg.client_request_id ?? 'NULL', timestamp: msg.finished_at, user_prompt: extractUserPromptFromRequest(msg.request), assistant_response: extractAssistantResponseFromResponse(msg.response), diff --git a/web/src/app/api/v1/chat/completions/route.ts b/web/src/app/api/v1/chat/completions/route.ts new file mode 100644 index 0000000000..9d10069ec0 --- /dev/null +++ b/web/src/app/api/v1/chat/completions/route.ts @@ -0,0 +1,114 @@ +import { getUserUsageData } from '@codebuff/billing/usage-service' +import { getErrorObject } from '@codebuff/common/util/error' +import { NextResponse } from 'next/server' + +import type { NextRequest } from 'next/server' + +import { getAgentRunFromId } from '@/db/agent-run' +import { getUserInfoFromApiKey } from '@/db/user' +import { handleOpenRouterStream } from '@/llm-api/openrouter' +import { extractApiKeyFromHeader } from '@/util/auth' +import { logger } from '@/util/logger' + +export async function POST(req: NextRequest) { + try { + const body = await req.json() + + const apiKey = extractApiKeyFromHeader(req) + + if (!apiKey) { + return NextResponse.json({ message: 'Unauthorized' }, { status: 401 }) + } + + const userInfo = await getUserInfoFromApiKey({ apiKey, fields: ['id'] }) + if (!userInfo) { + return NextResponse.json( + { message: 'Invalid Codebuff API key' }, + { status: 401 } + ) + } + + const userId = userInfo.id + const { + balance: { totalRemaining }, + nextQuotaReset, + } = await getUserUsageData(userId) + if (totalRemaining <= 0) { + return NextResponse.json( + { + message: `Insufficient credits. Please add credits at ${process.env.NEXT_PUBLIC_APP_URL}/usage or wait for your next cycle to begin (${nextQuotaReset}).`, + }, + { status: 402 } + ) + } + + if (!body.stream) { + return NextResponse.json( + { message: 'Not implemented. Use stream=true.' }, + { status: 500 } + ) + } + + const runIdFromBody: string | undefined = + body.codebuff_metadata?.agent_run_id + if (!runIdFromBody || typeof runIdFromBody !== 'string') { + return NextResponse.json( + { message: 'No agentRunId found in request body' }, + { status: 400 } + ) + } + + const agentRun = await getAgentRunFromId({ + agentRunId: runIdFromBody, + userId, + fields: ['agent_id', 'status'], + }) + if (!agentRun) { + return NextResponse.json( + { message: `agentRunId Not Found: ${runIdFromBody}` }, + { status: 400 } + ) + } + + const { agent_id: agentId, status: agentRunStatus } = agentRun + + if (agentRunStatus !== 'running') { + return NextResponse.json( + { message: `agentRunId Not Running: ${runIdFromBody}` }, + { status: 400 } + ) + } + + try { + const stream = await handleOpenRouterStream({ + body, + userId, + agentId, + }) + + return new NextResponse(stream, { + headers: { + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache', + Connection: 'keep-alive', + 'Access-Control-Allow-Origin': '*', + }, + }) + } catch (error) { + logger.error(getErrorObject(error), 'Error setting up OpenRouter stream:') + return NextResponse.json( + { error: 'Failed to initialize stream' }, + { status: 500 } + ) + } + } catch (error) { + logger.error( + getErrorObject(error), + 'Error processing chat completions request:' + ) + return NextResponse.json( + { error: 'Internal server error' }, + { status: 500 } + ) + } +} diff --git a/web/src/db/agent-run.ts b/web/src/db/agent-run.ts new file mode 100644 index 0000000000..26bb366276 --- /dev/null +++ b/web/src/db/agent-run.ts @@ -0,0 +1,43 @@ +import db from '@codebuff/common/db' +import * as schema from '@codebuff/common/db/schema' +import { eq, and } from 'drizzle-orm' + +import type { InferSelectModel } from 'drizzle-orm' + +type AgentRunTable = typeof schema.agentRun +type AgentRunColumn = AgentRunTable['_']['columns'] +type AgentRun = InferSelectModel + +export async function getAgentRunFromId< + T extends readonly (keyof AgentRunColumn)[], +>({ + agentRunId, + userId, + fields, +}: { + agentRunId: string + userId: string + fields: T +}): Promise< + | { + [K in T[number]]: AgentRun[K] + } + | undefined +> { + const selection = Object.fromEntries( + fields.map((field) => [field, schema.agentRun[field]]) + ) as { [K in T[number]]: AgentRunColumn[K] } + + const rows = await db + .select({ selection }) + .from(schema.agentRun) + .where( + and( + eq(schema.agentRun.id, agentRunId), + eq(schema.agentRun.user_id, userId) + ) + ) + .limit(1) + + return rows[0]?.selection +} diff --git a/web/src/db/user.ts b/web/src/db/user.ts new file mode 100644 index 0000000000..64e04fd5a0 --- /dev/null +++ b/web/src/db/user.ts @@ -0,0 +1,39 @@ +import db from '@codebuff/common/db' +import * as schema from '@codebuff/common/db/schema' +import { eq } from 'drizzle-orm' + +import type { InferSelectModel } from 'drizzle-orm' + +type UserTable = typeof schema.user +type UserColumn = UserTable['_']['columns'] +type User = InferSelectModel + +export async function getUserInfoFromApiKey< + T extends readonly (keyof UserColumn)[], +>({ + apiKey, + fields, +}: { + apiKey: string + fields: T +}): Promise< + | { + [K in T[number]]: User[K] + } + | undefined +> { + // Build a typed selection object for user columns + const userSelection = Object.fromEntries( + fields.map((field) => [field, schema.user[field]]) + ) as { [K in T[number]]: UserColumn[K] } + + const rows = await db + .select({ user: userSelection }) // <-- important: nest under 'user' + .from(schema.user) + .leftJoin(schema.session, eq(schema.user.id, schema.session.userId)) + .where(eq(schema.session.sessionToken, apiKey)) + .limit(1) + + // Drizzle returns { user: ..., session: ... }, we return only the user part + return rows[0]?.user +} diff --git a/web/src/llm-api/openrouter.ts b/web/src/llm-api/openrouter.ts new file mode 100644 index 0000000000..4de86b9321 --- /dev/null +++ b/web/src/llm-api/openrouter.ts @@ -0,0 +1,331 @@ +import { + insertMessage as insertMessageIntoBigquery, + setupBigQuery, +} from '@codebuff/bigquery' +import { consumeCreditsAndAddAgentStep as consumeCreditsAndAddMessage } from '@codebuff/billing' +import { getErrorObject } from '@codebuff/common/util/error' +import { env } from '@codebuff/internal/env' + +import { OpenRouterStreamChatCompletionChunkSchema } from './type/openrouter' + +import type { OpenRouterStreamChatCompletionChunk } from './type/openrouter' + +import { logger } from '@/util/logger' + +type StreamState = { responseText: string; reasoningText: string } + +const PROFIT_MARGIN = 0.055 + +export async function handleOpenRouterStream({ + body, + userId, + agentId, +}: { + body: any + userId: string + agentId: string +}) { + // Ensure usage tracking is enabled + if (body.usage === undefined) { + body.usage = {} + } + body.usage.include = true + + const startTime = new Date() + let clientId: string | null + if ( + body.codebuff_metadata?.client_id && + typeof body.codebuff_metadata?.client_id === 'string' + ) { + clientId = body.codebuff_metadata.client_id + } else { + logger.warn({ body }, 'Received request without client_id') + clientId = null + } + let clientRequestId: string | null + if ( + body.codebuff_metadata?.client_request_id && + typeof body.codebuff_metadata?.client_request_id === 'string' + ) { + clientRequestId = body.codebuff_metadata.client_request_id + } else { + logger.warn({ body }, 'Received request without client_request_id') + clientRequestId = null + } + + const response = await fetch( + 'https://openrouter.ai/api/v1/chat/completions', + { + method: 'POST', + headers: { + Authorization: `Bearer ${env.OPEN_ROUTER_API_KEY}`, + 'HTTP-Referer': 'https://codebuff.com', + 'X-Title': 'Codebuff', + 'Content-Type': 'application/json', + }, + body: JSON.stringify(body), + } + ) + + if (!response.ok) { + throw new Error(`OpenRouter API error: ${response.statusText}`) + } + + const reader = response.body?.getReader() + if (!reader) { + throw new Error('Failed to get response reader') + } + + let heartbeatInterval: NodeJS.Timeout + let state: StreamState = { responseText: '', reasoningText: '' } + let clientDisconnected = false + + // Create a ReadableStream that Next.js can handle + const stream = new ReadableStream({ + async start(controller) { + const decoder = new TextDecoder() + let buffer = '' + + // Send initial connection message + controller.enqueue( + new TextEncoder().encode(`: connected ${new Date().toISOString()}\n`) + ) + + // Start heartbeat + heartbeatInterval = setInterval(() => { + if (!clientDisconnected) { + try { + controller.enqueue( + new TextEncoder().encode( + `: heartbeat ${new Date().toISOString()}\n\n` + ) + ) + } catch { + // client disconnected, ignore error + } + } + }, 30000) + + try { + while (true) { + const { done, value } = await reader.read() + + if (done) { + break + } + + buffer += decoder.decode(value, { stream: true }) + let lineEnd = buffer.indexOf('\n') + + while (lineEnd !== -1) { + const line = buffer.slice(0, lineEnd + 1) + buffer = buffer.slice(lineEnd + 1) + + state = await handleLine({ + userId, + agentId, + clientId, + clientRequestId, + startTime, + request: body, + line, + state, + }) + + if (!clientDisconnected) { + try { + controller.enqueue(new TextEncoder().encode(line)) + } catch (error) { + logger.warn( + 'Client disconnected during stream, continuing for billing' + ) + clientDisconnected = true + } + } + + lineEnd = buffer.indexOf('\n') + } + } + + if (!clientDisconnected) { + controller.close() + } + } catch (error) { + if (!clientDisconnected) { + controller.error(error) + } else { + logger.warn( + getErrorObject(error), + 'Error after client disconnect in OpenRouter stream' + ) + } + } finally { + clearInterval(heartbeatInterval) + } + }, + cancel() { + clearInterval(heartbeatInterval) + clientDisconnected = true + logger.warn( + 'Client cancelled stream, continuing OpenRouter consumption for billing' + ) + }, + }) + + return stream +} + +async function handleLine({ + userId, + agentId, + clientId, + clientRequestId, + startTime, + request, + line, + state, +}: { + userId: string + agentId: string + clientId: string | null + clientRequestId: string | null + startTime: Date + request: unknown + line: string + state: StreamState +}): Promise { + if (!line.startsWith('data: ')) { + return state + } + + const raw = line.slice('data: '.length) + if (raw === '[DONE]\n') { + return state + } + + // Parse the string into an object + let obj + try { + obj = JSON.parse(raw) + } catch (error) { + logger.warn( + `Received non-JSON OpenRouter response: ${JSON.stringify(getErrorObject(error), null, 2)}` + ) + return state + } + + // Extract usage + const parsed = OpenRouterStreamChatCompletionChunkSchema.safeParse(obj) + if (!parsed.success) { + logger.warn( + `Unable to parse OpenRotuer response: ${JSON.stringify(getErrorObject(parsed.error), null, 2)}` + ) + return state + } + + return await handleResponse({ + userId, + agentId, + clientId, + clientRequestId, + startTime, + request, + data: parsed.data, + state, + }) +} + +async function handleResponse({ + userId, + agentId, + clientId, + clientRequestId, + startTime, + request, + data, + state, +}: { + userId: string + agentId: string + clientId: string | null + clientRequestId: string | null + startTime: Date + request: unknown + data: OpenRouterStreamChatCompletionChunk + state: StreamState +}): Promise { + state = await handleStreamChunk({ data, state }) + + if ('error' in data || !data.usage) { + // Stream not finished + return state + } + const usage = data.usage + + // do not await this + setupBigQuery().then(async () => { + const success = await insertMessageIntoBigquery({ + id: data.id, + user_id: userId, + finished_at: new Date(), + created_at: startTime, + request, + reasoning_text: state.reasoningText, + response: state.responseText, + output_tokens: usage.completion_tokens, + reasoning_tokens: usage.completion_tokens_details?.reasoning_tokens, + cost: usage.cost, + upstream_inference_cost: usage.cost_details?.upstream_inference_cost, + input_tokens: usage.prompt_tokens, + cache_read_input_tokens: usage.prompt_tokens_details?.cached_tokens, + }) + if (!success) { + logger.error({ request }, 'Failed to insert message into BigQuery') + } + }) + const openRouterCost = usage.cost ?? 0 + const upstreamCost = usage.cost_details?.upstream_inference_cost ?? 0 + const cost = openRouterCost + upstreamCost + + await consumeCreditsAndAddMessage({ + messageId: data.id, + userId, + agentId, + clientId, + clientRequestId, + startTime, + model: data.model, + reasoningText: state.reasoningText, + response: state.responseText, + cost, + credits: Math.round(cost * 100 * (1 + PROFIT_MARGIN)), + inputTokens: usage.prompt_tokens, + cacheCreationInputTokens: null, + cacheReadInputTokens: usage.prompt_tokens_details?.cached_tokens ?? 0, + reasoningTokens: usage.completion_tokens_details?.reasoning_tokens ?? null, + outputTokens: usage.completion_tokens, + }) + + return state +} + +async function handleStreamChunk({ + data, + state, +}: { + data: OpenRouterStreamChatCompletionChunk + state: StreamState +}): Promise { + if ('error' in data) { + logger.warn({ streamChunk: data }, 'Received error from OpenRouter') + return state + } + + if (!data.choices.length) { + logger.warn({ streamChunk: data }, 'Received empty choices from OpenRouter') + } + const choice = data.choices[0] + state.responseText += choice.delta?.content ?? '' + state.reasoningText += choice.delta?.reasoning ?? '' + return state +} diff --git a/web/src/llm-api/type/openrouter.ts b/web/src/llm-api/type/openrouter.ts new file mode 100644 index 0000000000..e65b7773ca --- /dev/null +++ b/web/src/llm-api/type/openrouter.ts @@ -0,0 +1,157 @@ +/* forked from https://github.com/OpenRouterTeam/ai-sdk-provider/tree/b23f2d580dc0688e5af1124e68c0e98b892e58fb/src/schemas */ +import z from 'zod/v4' + +export enum ReasoningDetailType { + Summary = 'reasoning.summary', + Encrypted = 'reasoning.encrypted', + Text = 'reasoning.text', +} + +export const ReasoningDetailSummarySchema = z.object({ + type: z.literal(ReasoningDetailType.Summary), + summary: z.string(), +}) +export type ReasoningDetailSummary = z.infer< + typeof ReasoningDetailSummarySchema +> + +export const ReasoningDetailEncryptedSchema = z.object({ + type: z.literal(ReasoningDetailType.Encrypted), + data: z.string(), +}) +export type ReasoningDetailEncrypted = z.infer< + typeof ReasoningDetailEncryptedSchema +> + +export const ReasoningDetailTextSchema = z.object({ + type: z.literal(ReasoningDetailType.Text), + text: z.string().nullish(), + signature: z.string().nullish(), +}) + +export type ReasoningDetailText = z.infer + +export const ReasoningDetailUnionSchema = z.union([ + ReasoningDetailSummarySchema, + ReasoningDetailEncryptedSchema, + ReasoningDetailTextSchema, +]) + +const ReasoningDetailsWithUnknownSchema = z.union([ + ReasoningDetailUnionSchema, + z.unknown().transform(() => null), +]) + +export type ReasoningDetailUnion = z.infer + +export const ReasoningDetailArraySchema = z + .array(ReasoningDetailsWithUnknownSchema) + .transform((d) => d.filter((d): d is ReasoningDetailUnion => !!d)) +const OpenRouterChatCompletionBaseResponseSchema = z.object({ + id: z.string(), + model: z.string(), + provider: z.string(), + created: z.number(), + usage: z + .object({ + prompt_tokens: z.number(), + prompt_tokens_details: z + .object({ + cached_tokens: z.number(), + }) + .nullish(), + completion_tokens: z.number(), + completion_tokens_details: z + .object({ + reasoning_tokens: z.number(), + }) + .nullish(), + total_tokens: z.number(), + cost: z.number().optional(), + cost_details: z + .object({ + upstream_inference_cost: z.number().nullish(), + }) + .nullish(), + }) + .nullish(), +}) + +export const OpenRouterErrorResponseSchema = z.object({ + error: z.object({ + code: z.union([z.string(), z.number()]).nullable().optional().default(null), + message: z.string(), + type: z.string().nullable().optional().default(null), + param: z.any().nullable().optional().default(null), + }), +}) + +export const OpenRouterStreamChatCompletionChunkSchema = z.union([ + OpenRouterChatCompletionBaseResponseSchema.extend({ + choices: z.array( + z.object({ + delta: z + .object({ + role: z.enum(['assistant']).optional(), + content: z.string().nullish(), + reasoning: z.string().nullish().optional(), + reasoning_details: ReasoningDetailArraySchema.nullish(), + tool_calls: z + .array( + z.object({ + index: z.number().nullish(), + id: z.string().nullish(), + type: z.literal('function').optional(), + function: z.object({ + name: z.string().nullish(), + arguments: z.string().nullish(), + }), + }) + ) + .nullish(), + + annotations: z + .array( + z.object({ + type: z.enum(['url_citation']), + url_citation: z.object({ + end_index: z.number(), + start_index: z.number(), + title: z.string(), + url: z.string(), + content: z.string().optional(), + }), + }) + ) + .nullish(), + }) + .nullish(), + logprobs: z + .object({ + content: z + .array( + z.object({ + token: z.string(), + logprob: z.number(), + top_logprobs: z.array( + z.object({ + token: z.string(), + logprob: z.number(), + }) + ), + }) + ) + .nullable(), + }) + .nullish(), + finish_reason: z.string().nullable().optional(), + index: z.number().nullish(), + }) + ), + }), + OpenRouterErrorResponseSchema, +]) + +export type OpenRouterStreamChatCompletionChunk = z.infer< + typeof OpenRouterStreamChatCompletionChunkSchema +> diff --git a/web/src/util/auth.ts b/web/src/util/auth.ts new file mode 100644 index 0000000000..6c843b82dd --- /dev/null +++ b/web/src/util/auth.ts @@ -0,0 +1,20 @@ +import type { NextRequest } from 'next/server' + +/** + * Extract api key from x-codebuff-api-key header or authorization header + */ +export function extractApiKeyFromHeader(req: NextRequest): string | undefined { + const token = req.headers.get('x-codebuff-api-key') + if (typeof token === 'string' && token) { + return token + } + + const authorization = req.headers.get('Authorization') + if (!authorization) { + return undefined + } + if (!authorization.startsWith('Bearer ')) { + return undefined + } + return authorization.slice('Bearer '.length) +}