teamcopilot 0.2.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/.env.example +6 -0
  2. package/dist/chat/index.js +18 -0
  3. package/dist/frontend/assets/{cssMode-D5XpZugr.js → cssMode-BTzeyxuZ.js} +1 -1
  4. package/dist/frontend/assets/{freemarker2-1bzrSLLO.js → freemarker2-JV7VLJuz.js} +1 -1
  5. package/dist/frontend/assets/{handlebars-BrCOyfYR.js → handlebars-Wq0xrjB8.js} +1 -1
  6. package/dist/frontend/assets/{html-CeIQZCI8.js → html-KQAZW_HC.js} +1 -1
  7. package/dist/frontend/assets/{htmlMode-B6ILgHd7.js → htmlMode-DRJ1_AV3.js} +1 -1
  8. package/dist/frontend/assets/{index-B0u3xzuM.js → index-BHvb2gDp.js} +232 -232
  9. package/dist/frontend/assets/index-CQhgzoZH.css +1 -0
  10. package/dist/frontend/assets/{javascript-BBiK7v6P.js → javascript-BwwBtsWD.js} +1 -1
  11. package/dist/frontend/assets/{jsonMode-DUvNLAsU.js → jsonMode-D1JS0FC-.js} +1 -1
  12. package/dist/frontend/assets/{liquid-DbDUmqHQ.js → liquid-CtZHahU2.js} +1 -1
  13. package/dist/frontend/assets/{mdx-DhBBmLRc.js → mdx-DtoZ-9fa.js} +1 -1
  14. package/dist/frontend/assets/{python-DjRmQTqo.js → python-DsLd8hBF.js} +1 -1
  15. package/dist/frontend/assets/{razor-B0p8vOFC.js → razor-ByeasSZw.js} +1 -1
  16. package/dist/frontend/assets/{tsMode-CmtykvH8.js → tsMode-B8ZZzbQC.js} +1 -1
  17. package/dist/frontend/assets/{typescript-ByGe7b1m.js → typescript-DafR78wI.js} +1 -1
  18. package/dist/frontend/assets/{xml-BJ90e21o.js → xml-BIY1cH0j.js} +1 -1
  19. package/dist/frontend/assets/{yaml-BJKnMsp4.js → yaml-CitqxWJM.js} +1 -1
  20. package/dist/frontend/index.html +2 -2
  21. package/dist/index.js +2 -0
  22. package/dist/usage/index.js +14 -0
  23. package/dist/utils/chat-usage.js +258 -0
  24. package/dist/utils/model-pricing.js +107 -0
  25. package/package.json +2 -2
  26. package/prisma/generated/client/edge.js +16 -3
  27. package/prisma/generated/client/index-browser.js +13 -0
  28. package/prisma/generated/client/index.d.ts +1995 -259
  29. package/prisma/generated/client/index.js +16 -3
  30. package/prisma/generated/client/package.json +1 -1
  31. package/prisma/generated/client/schema.prisma +14 -0
  32. package/prisma/generated/client/wasm.js +16 -3
  33. package/prisma/migrations/20260408073541_add_chat_session_usage/migration.sql +11 -0
  34. package/prisma/migrations/20260409044955_add_last_synced_message_id_to_chat_session_usage/migration.sql +2 -0
  35. package/prisma/migrations/20260409045848_add_provider_id_to_chat_session_usage/migration.sql +2 -0
  36. package/prisma/migrations/20260409051857_require_provider_and_last_synced_message_id_in_chat_session_usage/migration.sql +27 -0
  37. package/prisma/schema.prisma +14 -0
  38. package/dist/frontend/assets/index-DwTKL-xp.css +0 -1
@@ -0,0 +1,258 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.syncChatSessionUsage = syncChatSessionUsage;
7
+ exports.buildUsageOverview = buildUsageOverview;
8
+ const client_1 = __importDefault(require("../prisma/client"));
9
+ const assert_1 = require("./assert");
10
+ const opencode_client_1 = require("./opencode-client");
11
+ const model_pricing_1 = require("./model-pricing");
12
+ function assertNonNegativeNumber(value, label) {
13
+ (0, assert_1.assertCondition)(typeof value === "number" && Number.isFinite(value) && value >= 0, `${label} must be a non-negative number`);
14
+ }
15
+ function assertSessionMessages(value) {
16
+ (0, assert_1.assertCondition)(Array.isArray(value), "Session messages response is not an array");
17
+ for (const [index, message] of value.entries()) {
18
+ (0, assert_1.assertCondition)(message !== null && typeof message === "object", `Session message at index ${index} must be an object`);
19
+ const messageRecord = message;
20
+ (0, assert_1.assertCondition)(messageRecord.info !== null && typeof messageRecord.info === "object", `Session message at index ${index} is missing info`);
21
+ const info = messageRecord.info;
22
+ (0, assert_1.assertCondition)(info.role === "user" || info.role === "assistant", `Session message at index ${index} has invalid role`);
23
+ (0, assert_1.assertCondition)(info.time !== null && typeof info.time === "object", `Session message at index ${index} is missing time`);
24
+ const time = info.time;
25
+ assertNonNegativeNumber(time.created, `Session message at index ${index} time.created`);
26
+ (0, assert_1.assertCondition)(typeof info.id === "string" && info.id.length > 0, `Session message at index ${index} is missing id`);
27
+ if (info.role === "assistant") {
28
+ (0, assert_1.assertCondition)(typeof info.modelID === "string" && info.modelID.length > 0, `Assistant message at index ${index} is missing modelID`);
29
+ (0, assert_1.assertCondition)(typeof info.providerID === "string" && info.providerID.length > 0, `Assistant message at index ${index} is missing providerID`);
30
+ (0, assert_1.assertCondition)(info.tokens !== null && typeof info.tokens === "object", `Assistant message at index ${index} is missing tokens`);
31
+ const tokens = info.tokens;
32
+ assertNonNegativeNumber(tokens.input, `Assistant message at index ${index} tokens.input`);
33
+ assertNonNegativeNumber(tokens.output, `Assistant message at index ${index} tokens.output`);
34
+ assertNonNegativeNumber(tokens.reasoning, `Assistant message at index ${index} tokens.reasoning`);
35
+ (0, assert_1.assertCondition)(tokens.cache !== null && typeof tokens.cache === "object", `Assistant message at index ${index} tokens.cache is missing`);
36
+ const cache = tokens.cache;
37
+ assertNonNegativeNumber(cache.read, `Assistant message at index ${index} tokens.cache.read`);
38
+ assertNonNegativeNumber(cache.write, `Assistant message at index ${index} tokens.cache.write`);
39
+ if (time.completed !== undefined) {
40
+ assertNonNegativeNumber(time.completed, `Assistant message at index ${index} time.completed`);
41
+ }
42
+ }
43
+ }
44
+ }
45
+ async function syncChatSessionUsage(chatSessionId, opencodeSessionId) {
46
+ const client = await (0, opencode_client_1.getOpencodeClient)();
47
+ const result = await client.session.messages({
48
+ path: { id: opencodeSessionId }
49
+ });
50
+ if (result.error) {
51
+ throw new Error("Failed to load session messages for usage sync");
52
+ }
53
+ assertSessionMessages(result.data);
54
+ const messages = result.data;
55
+ const existingUsage = await client_1.default.chat_session_usage.findUnique({
56
+ where: {
57
+ chat_session_id: chatSessionId,
58
+ }
59
+ });
60
+ let startIndex = 0;
61
+ if (existingUsage) {
62
+ const lastSyncedIndex = messages.findIndex((message) => (message.info.role === "assistant"
63
+ && message.info.id === existingUsage.last_synced_message_id));
64
+ if (lastSyncedIndex === -1) {
65
+ return;
66
+ }
67
+ startIndex = lastSyncedIndex + 1;
68
+ }
69
+ let deltaInputTokens = 0;
70
+ let deltaOutputTokens = 0;
71
+ let deltaCachedTokens = 0;
72
+ let latestProcessedAssistantMessageId = null;
73
+ let latestProcessedProviderId = null;
74
+ let latestProcessedModelId = null;
75
+ for (const message of messages.slice(startIndex)) {
76
+ const info = message.info;
77
+ if (info.role !== "assistant") {
78
+ continue;
79
+ }
80
+ deltaInputTokens += info.tokens.input;
81
+ deltaOutputTokens += info.tokens.output;
82
+ deltaCachedTokens += info.tokens.cache.read;
83
+ latestProcessedAssistantMessageId = info.id;
84
+ latestProcessedProviderId = info.providerID;
85
+ latestProcessedModelId = info.modelID;
86
+ }
87
+ if (latestProcessedAssistantMessageId === null
88
+ || latestProcessedProviderId === null
89
+ || latestProcessedModelId === null) {
90
+ return;
91
+ }
92
+ const deltaCostUsd = (0, model_pricing_1.calculateEstimatedCostUsd)({
93
+ providerId: latestProcessedProviderId,
94
+ modelId: latestProcessedModelId,
95
+ inputTokens: deltaInputTokens,
96
+ outputTokens: deltaOutputTokens,
97
+ cachedTokens: deltaCachedTokens,
98
+ });
99
+ const nextInputTokens = (existingUsage?.input_tokens ?? 0) + deltaInputTokens;
100
+ const nextOutputTokens = (existingUsage?.output_tokens ?? 0) + deltaOutputTokens;
101
+ const nextCachedTokens = (existingUsage?.cached_tokens ?? 0) + deltaCachedTokens;
102
+ const nextCostUsd = (existingUsage?.cost_usd ?? 0) + deltaCostUsd;
103
+ await client_1.default.chat_session_usage.upsert({
104
+ where: {
105
+ chat_session_id: chatSessionId,
106
+ },
107
+ create: {
108
+ chat_session_id: chatSessionId,
109
+ last_synced_message_id: latestProcessedAssistantMessageId,
110
+ provider_id: latestProcessedProviderId,
111
+ input_tokens: nextInputTokens,
112
+ output_tokens: nextOutputTokens,
113
+ cached_tokens: nextCachedTokens,
114
+ cost_usd: nextCostUsd,
115
+ model_id: latestProcessedModelId,
116
+ updated_at: BigInt(Date.now()),
117
+ },
118
+ update: {
119
+ last_synced_message_id: latestProcessedAssistantMessageId,
120
+ provider_id: latestProcessedProviderId,
121
+ input_tokens: nextInputTokens,
122
+ output_tokens: nextOutputTokens,
123
+ cached_tokens: nextCachedTokens,
124
+ cost_usd: nextCostUsd,
125
+ model_id: latestProcessedModelId,
126
+ updated_at: BigInt(Date.now()),
127
+ }
128
+ });
129
+ }
130
+ function getRangeConfig(range) {
131
+ switch (range) {
132
+ case "24h":
133
+ return { bucketMs: 60 * 60 * 1000, windowMs: 24 * 60 * 60 * 1000 };
134
+ case "7d":
135
+ return { bucketMs: 24 * 60 * 60 * 1000, windowMs: 7 * 24 * 60 * 60 * 1000 };
136
+ case "30d":
137
+ return { bucketMs: 24 * 60 * 60 * 1000, windowMs: 30 * 24 * 60 * 60 * 1000 };
138
+ case "90d":
139
+ return { bucketMs: 24 * 60 * 60 * 1000, windowMs: 90 * 24 * 60 * 60 * 1000 };
140
+ }
141
+ }
142
+ function normalizeUsageRange(value) {
143
+ if (value === "24h" || value === "7d" || value === "30d" || value === "90d") {
144
+ return value;
145
+ }
146
+ return "7d";
147
+ }
148
+ function getBucketStart(timestamp, bucketMs) {
149
+ return Math.floor(timestamp / bucketMs) * bucketMs;
150
+ }
151
+ async function buildUsageOverview(rawRange) {
152
+ const range = normalizeUsageRange(rawRange);
153
+ const { bucketMs, windowMs } = getRangeConfig(range);
154
+ const now = Date.now();
155
+ const rangeStart = now - windowMs;
156
+ const usageRows = await client_1.default.chat_session_usage.findMany({
157
+ where: {
158
+ session: {
159
+ updated_at: {
160
+ gte: BigInt(rangeStart),
161
+ lte: BigInt(now),
162
+ }
163
+ }
164
+ },
165
+ include: {
166
+ session: {
167
+ select: {
168
+ updated_at: true,
169
+ }
170
+ }
171
+ }
172
+ });
173
+ const bucketMap = new Map();
174
+ const modelMap = new Map();
175
+ let totalInputTokens = 0;
176
+ let totalOutputTokens = 0;
177
+ let totalCachedTokens = 0;
178
+ let totalCostUsd = 0;
179
+ for (const row of usageRows) {
180
+ const sessionUpdatedAt = Number(row.session.updated_at);
181
+ const bucketStart = getBucketStart(sessionUpdatedAt, bucketMs);
182
+ const bucket = bucketMap.get(bucketStart) ?? {
183
+ bucket_start: bucketStart,
184
+ input_tokens: 0,
185
+ output_tokens: 0,
186
+ cached_tokens: 0,
187
+ cost_usd: 0,
188
+ session_count: 0,
189
+ };
190
+ bucket.input_tokens += row.input_tokens;
191
+ bucket.output_tokens += row.output_tokens;
192
+ bucket.cached_tokens += row.cached_tokens;
193
+ bucket.cost_usd += row.cost_usd;
194
+ bucket.session_count += 1;
195
+ bucketMap.set(bucketStart, bucket);
196
+ const modelKey = `${row.provider_id}:${row.model_id}`;
197
+ const model = modelMap.get(modelKey) ?? {
198
+ provider_id: row.provider_id,
199
+ model_id: row.model_id,
200
+ input_tokens: 0,
201
+ output_tokens: 0,
202
+ cached_tokens: 0,
203
+ cost_usd: 0,
204
+ session_count: 0,
205
+ pricing_available: (0, model_pricing_1.getModelPricing)(row.provider_id, row.model_id) !== null,
206
+ };
207
+ model.input_tokens += row.input_tokens;
208
+ model.output_tokens += row.output_tokens;
209
+ model.cached_tokens += row.cached_tokens;
210
+ model.cost_usd += row.cost_usd;
211
+ model.session_count += 1;
212
+ modelMap.set(modelKey, model);
213
+ totalInputTokens += row.input_tokens;
214
+ totalOutputTokens += row.output_tokens;
215
+ totalCachedTokens += row.cached_tokens;
216
+ totalCostUsd += row.cost_usd;
217
+ }
218
+ const bucketStarts = [];
219
+ for (let time = getBucketStart(rangeStart, bucketMs); time <= getBucketStart(now, bucketMs); time += bucketMs) {
220
+ bucketStarts.push(time);
221
+ }
222
+ const timeseries = bucketStarts.map((bucketStart) => bucketMap.get(bucketStart) ?? {
223
+ bucket_start: bucketStart,
224
+ input_tokens: 0,
225
+ output_tokens: 0,
226
+ cached_tokens: 0,
227
+ cost_usd: 0,
228
+ session_count: 0,
229
+ });
230
+ const models = Array.from(modelMap.values()).sort((a, b) => b.cost_usd - a.cost_usd);
231
+ const pricing = {};
232
+ for (const model of models) {
233
+ const modelPricing = (0, model_pricing_1.getModelPricing)(model.provider_id, model.model_id);
234
+ if (!modelPricing) {
235
+ continue;
236
+ }
237
+ pricing[`${model.provider_id}:${model.model_id}`] = {
238
+ provider_id: model.provider_id,
239
+ input_per_million_usd: modelPricing.inputPerMillionUsd,
240
+ cached_input_per_million_usd: modelPricing.cachedInputPerMillionUsd,
241
+ output_per_million_usd: modelPricing.outputPerMillionUsd,
242
+ };
243
+ }
244
+ return {
245
+ range,
246
+ estimated: true,
247
+ summary: {
248
+ total_input_tokens: totalInputTokens,
249
+ total_output_tokens: totalOutputTokens,
250
+ total_cached_tokens: totalCachedTokens,
251
+ total_cost_usd: totalCostUsd,
252
+ session_count: usageRows.length,
253
+ },
254
+ timeseries,
255
+ models,
256
+ pricing,
257
+ };
258
+ }
@@ -0,0 +1,107 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getModelPricing = getModelPricing;
4
+ exports.calculateEstimatedCostUsd = calculateEstimatedCostUsd;
5
+ const MODEL_PRICING = {
6
+ openai: {
7
+ "gpt-5.4": {
8
+ inputPerMillionUsd: 2.5,
9
+ cachedInputPerMillionUsd: 0.25,
10
+ outputPerMillionUsd: 15,
11
+ },
12
+ "gpt-5.4-mini": {
13
+ inputPerMillionUsd: 0.75,
14
+ cachedInputPerMillionUsd: 0.075,
15
+ outputPerMillionUsd: 4.5,
16
+ },
17
+ "gpt-5.4-nano": {
18
+ inputPerMillionUsd: 0.2,
19
+ cachedInputPerMillionUsd: 0.02,
20
+ outputPerMillionUsd: 1.25,
21
+ },
22
+ "gpt-5.3-chat-latest": {
23
+ inputPerMillionUsd: 1.75,
24
+ cachedInputPerMillionUsd: 0.175,
25
+ outputPerMillionUsd: 14,
26
+ },
27
+ "gpt-5.3-codex": {
28
+ inputPerMillionUsd: 1.75,
29
+ cachedInputPerMillionUsd: 0.175,
30
+ outputPerMillionUsd: 14,
31
+ },
32
+ "gpt-5.1": {
33
+ inputPerMillionUsd: 1.25,
34
+ cachedInputPerMillionUsd: 0.125,
35
+ outputPerMillionUsd: 10,
36
+ },
37
+ "gpt-5": {
38
+ inputPerMillionUsd: 1.25,
39
+ cachedInputPerMillionUsd: 0.125,
40
+ outputPerMillionUsd: 10,
41
+ },
42
+ "gpt-5-mini": {
43
+ inputPerMillionUsd: 0.25,
44
+ cachedInputPerMillionUsd: 0.025,
45
+ outputPerMillionUsd: 2,
46
+ },
47
+ "gpt-5-nano": {
48
+ inputPerMillionUsd: 0.05,
49
+ cachedInputPerMillionUsd: 0.005,
50
+ outputPerMillionUsd: 0.4,
51
+ }
52
+ },
53
+ };
54
+ function assertNonNegativeNumber(value, label) {
55
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
56
+ throw new Error(`${label} must be a non-negative number`);
57
+ }
58
+ }
59
+ function parseOptionalNonNegativeNumber(raw, label) {
60
+ if (raw === undefined || raw.length === 0) {
61
+ return null;
62
+ }
63
+ const parsed = Number(raw);
64
+ assertNonNegativeNumber(parsed, label);
65
+ return parsed;
66
+ }
67
+ function getPricingOverrideForConfiguredModel(providerId, modelId) {
68
+ const configuredModel = process.env.OPENCODE_MODEL;
69
+ const [configuredProviderId, ...configuredModelParts] = configuredModel.split("/");
70
+ const configuredModelId = configuredModelParts.join("/");
71
+ if (!configuredProviderId || !configuredModelId) {
72
+ throw new Error("OPENCODE_MODEL must be in the format <provider>/<model>");
73
+ }
74
+ if (providerId !== configuredProviderId || modelId !== configuredModelId) {
75
+ return null;
76
+ }
77
+ const inputPerMillionUsd = parseOptionalNonNegativeNumber(process.env.TEAMCOPILOT_MODEL_INPUT_PER_MILLION_USD, "TEAMCOPILOT_MODEL_INPUT_PER_MILLION_USD");
78
+ const cachedInputPerMillionUsd = parseOptionalNonNegativeNumber(process.env.TEAMCOPILOT_MODEL_CACHED_INPUT_PER_MILLION_USD, "TEAMCOPILOT_MODEL_CACHED_INPUT_PER_MILLION_USD");
79
+ const outputPerMillionUsd = parseOptionalNonNegativeNumber(process.env.TEAMCOPILOT_MODEL_OUTPUT_PER_MILLION_USD, "TEAMCOPILOT_MODEL_OUTPUT_PER_MILLION_USD");
80
+ if (inputPerMillionUsd === null && cachedInputPerMillionUsd === null && outputPerMillionUsd === null) {
81
+ return null;
82
+ }
83
+ if (inputPerMillionUsd === null || cachedInputPerMillionUsd === null || outputPerMillionUsd === null) {
84
+ throw new Error("TEAMCOPILOT_MODEL_INPUT_PER_MILLION_USD, TEAMCOPILOT_MODEL_CACHED_INPUT_PER_MILLION_USD, and TEAMCOPILOT_MODEL_OUTPUT_PER_MILLION_USD must either all be set or all be unset");
85
+ }
86
+ return {
87
+ inputPerMillionUsd,
88
+ cachedInputPerMillionUsd,
89
+ outputPerMillionUsd,
90
+ };
91
+ }
92
+ function getModelPricing(providerId, modelId) {
93
+ const override = getPricingOverrideForConfiguredModel(providerId, modelId);
94
+ if (override) {
95
+ return override;
96
+ }
97
+ return MODEL_PRICING[providerId]?.[modelId] ?? null;
98
+ }
99
+ function calculateEstimatedCostUsd(args) {
100
+ const pricing = getModelPricing(args.providerId, args.modelId);
101
+ if (!pricing) {
102
+ return 0;
103
+ }
104
+ return ((args.inputTokens * pricing.inputPerMillionUsd) / 1000000
105
+ + (args.cachedTokens * pricing.cachedInputPerMillionUsd) / 1000000
106
+ + (args.outputTokens * pricing.outputPerMillionUsd) / 1000000);
107
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "teamcopilot",
3
- "version": "0.2.2",
3
+ "version": "0.3.0",
4
4
  "description": "A shared AI Agent for Teams",
5
5
  "homepage": "https://teamcopilot.ai",
6
6
  "repository": {
@@ -24,7 +24,7 @@
24
24
  "scripts": {
25
25
  "clean": "rm -rf dist",
26
26
  "check:unused:functions": "tsc --noEmit --noUnusedLocals --noUnusedParameters",
27
- "check:unused:exports": "ts-prune -e -p tsconfig.json -i \"^src/types/shared/|^prisma/generated/|^src/index.ts:31 - createApp \\(used in module\\)$|^src/utils/secret-contract-validation.ts:45 - extractReferencedWorkflowSecrets \\(used in module\\)$|^src/utils/secret-contract-validation.ts:57 - extractReferencedSkillSecrets \\(used in module\\)$|^src/utils/secret-contract-validation.ts:158 - parseSkillRequiredSecrets \\(used in module\\)$\"",
27
+ "check:unused:exports": "ts-prune -e -p tsconfig.json -i \"^src/types/shared/|^prisma/generated/|^src/index.ts:[0-9]+ - createApp \\(used in module\\)$|^src/utils/secret-contract-validation.ts:45 - extractReferencedWorkflowSecrets \\(used in module\\)$|^src/utils/secret-contract-validation.ts:57 - extractReferencedSkillSecrets \\(used in module\\)$|^src/utils/secret-contract-validation.ts:158 - parseSkillRequiredSecrets \\(used in module\\)$\"",
28
28
  "check:unused": "npm run check:unused:functions && npm run check:unused:exports",
29
29
  "build": "npm run clean && npm run check:unused && cd frontend && npm run build && cd .. && prisma generate --schema prisma/schema.prisma && tsc && node scripts/copy-runtime-assets.mjs",
30
30
  "start": "node dist/index.js",