bopodev-api 0.1.13 → 0.1.14
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.
- package/LICENSE +1 -1
- package/package.json +4 -4
- package/src/lib/agent-config.ts +36 -1
- package/src/lib/opencode-model.ts +2 -26
- package/src/routes/agents.ts +22 -0
- package/src/routes/companies.ts +2 -0
- package/src/routes/observability.ts +50 -1
- package/src/scripts/onboard-seed.ts +4 -1
- package/src/services/governance-service.ts +9 -0
- package/src/services/heartbeat-service.ts +118 -19
- package/src/services/model-pricing.ts +217 -0
package/LICENSE
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bopodev-api",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.14",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -17,9 +17,9 @@
|
|
|
17
17
|
"nanoid": "^5.1.5",
|
|
18
18
|
"ws": "^8.19.0",
|
|
19
19
|
"zod": "^4.1.5",
|
|
20
|
-
"bopodev-
|
|
21
|
-
"bopodev-
|
|
22
|
-
"bopodev-
|
|
20
|
+
"bopodev-db": "0.1.14",
|
|
21
|
+
"bopodev-contracts": "0.1.14",
|
|
22
|
+
"bopodev-agent-sdk": "0.1.14"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
25
|
"@types/cors": "^2.8.19",
|
package/src/lib/agent-config.ts
CHANGED
|
@@ -39,10 +39,39 @@ export function requiresRuntimeCwd(providerType: string) {
|
|
|
39
39
|
providerType === "claude_code" ||
|
|
40
40
|
providerType === "cursor" ||
|
|
41
41
|
providerType === "opencode" ||
|
|
42
|
+
providerType === "gemini_cli" ||
|
|
42
43
|
providerType === "shell"
|
|
43
44
|
);
|
|
44
45
|
}
|
|
45
46
|
|
|
47
|
+
export function resolveDefaultRuntimeModelForProvider(providerType: string | undefined) {
|
|
48
|
+
const normalizedProviderType = providerType?.trim() ?? "";
|
|
49
|
+
if (normalizedProviderType === "claude_code") {
|
|
50
|
+
return "claude-sonnet-4-6";
|
|
51
|
+
}
|
|
52
|
+
if (normalizedProviderType === "codex") {
|
|
53
|
+
return "gpt-5.3-codex";
|
|
54
|
+
}
|
|
55
|
+
if (normalizedProviderType === "opencode") {
|
|
56
|
+
return "opencode/big-pickle";
|
|
57
|
+
}
|
|
58
|
+
if (normalizedProviderType === "gemini_cli") {
|
|
59
|
+
return "gemini-2.5-pro";
|
|
60
|
+
}
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function resolveRuntimeModelForProvider(
|
|
65
|
+
providerType: string | undefined,
|
|
66
|
+
runtimeModel: string | undefined
|
|
67
|
+
) {
|
|
68
|
+
const normalizedRuntimeModel = runtimeModel?.trim() || undefined;
|
|
69
|
+
if (normalizedRuntimeModel) {
|
|
70
|
+
return normalizedRuntimeModel;
|
|
71
|
+
}
|
|
72
|
+
return resolveDefaultRuntimeModelForProvider(providerType);
|
|
73
|
+
}
|
|
74
|
+
|
|
46
75
|
export function normalizeRuntimeConfig(input: {
|
|
47
76
|
runtimeConfig?: Partial<AgentRuntimeConfig>;
|
|
48
77
|
legacy?: LegacyRuntimeFields;
|
|
@@ -116,12 +145,15 @@ export function parseRuntimeConfigFromAgentRow(agent: Record<string, unknown>):
|
|
|
116
145
|
? timeoutSecFromColumn
|
|
117
146
|
: (toSeconds(fallback.timeoutMs) ?? 0);
|
|
118
147
|
|
|
148
|
+
const providerType = toText(agent.providerType);
|
|
149
|
+
const runtimeModel = resolveRuntimeModelForProvider(providerType, toText(agent.runtimeModel) ?? fallback.model);
|
|
150
|
+
|
|
119
151
|
return {
|
|
120
152
|
runtimeCommand: toText(agent.runtimeCommand) ?? fallback.command,
|
|
121
153
|
runtimeArgs,
|
|
122
154
|
runtimeCwd: toText(agent.runtimeCwd) ?? fallback.cwd,
|
|
123
155
|
runtimeEnv,
|
|
124
|
-
runtimeModel
|
|
156
|
+
runtimeModel,
|
|
125
157
|
runtimeThinkingEffort: parseThinkingEffort(agent.runtimeThinkingEffort),
|
|
126
158
|
bootstrapPrompt: toText(agent.bootstrapPrompt),
|
|
127
159
|
runtimeTimeoutSec: Math.max(0, timeoutSec),
|
|
@@ -165,6 +197,7 @@ function parseRuntimeFromStateBlob(raw: unknown) {
|
|
|
165
197
|
args?: string[];
|
|
166
198
|
cwd?: string;
|
|
167
199
|
env?: Record<string, string>;
|
|
200
|
+
model?: string;
|
|
168
201
|
timeoutMs?: number;
|
|
169
202
|
};
|
|
170
203
|
}
|
|
@@ -175,6 +208,7 @@ function parseRuntimeFromStateBlob(raw: unknown) {
|
|
|
175
208
|
args?: unknown;
|
|
176
209
|
cwd?: unknown;
|
|
177
210
|
env?: unknown;
|
|
211
|
+
model?: unknown;
|
|
178
212
|
timeoutMs?: unknown;
|
|
179
213
|
};
|
|
180
214
|
};
|
|
@@ -184,6 +218,7 @@ function parseRuntimeFromStateBlob(raw: unknown) {
|
|
|
184
218
|
args: Array.isArray(runtime.args) ? runtime.args.map((entry) => String(entry)) : undefined,
|
|
185
219
|
cwd: typeof runtime.cwd === "string" ? runtime.cwd : undefined,
|
|
186
220
|
env: toRecord(runtime.env),
|
|
221
|
+
model: typeof runtime.model === "string" && runtime.model.trim().length > 0 ? runtime.model.trim() : undefined,
|
|
187
222
|
timeoutMs: toNumber(runtime.timeoutMs)
|
|
188
223
|
};
|
|
189
224
|
} catch {
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { getAdapterModels } from "bopodev-agent-sdk";
|
|
2
1
|
import type { NormalizedRuntimeConfig } from "./agent-config";
|
|
3
2
|
|
|
4
3
|
export async function resolveOpencodeRuntimeModel(
|
|
@@ -6,30 +5,7 @@ export async function resolveOpencodeRuntimeModel(
|
|
|
6
5
|
runtimeConfig: NormalizedRuntimeConfig
|
|
7
6
|
): Promise<string | undefined> {
|
|
8
7
|
if (providerType !== "opencode") {
|
|
9
|
-
return runtimeConfig.runtimeModel;
|
|
8
|
+
return runtimeConfig.runtimeModel?.trim() || undefined;
|
|
10
9
|
}
|
|
11
|
-
|
|
12
|
-
return runtimeConfig.runtimeModel.trim();
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
const configured =
|
|
16
|
-
process.env.BOPO_OPENCODE_MODEL?.trim() ||
|
|
17
|
-
process.env.OPENCODE_MODEL?.trim() ||
|
|
18
|
-
undefined;
|
|
19
|
-
try {
|
|
20
|
-
const discovered = await getAdapterModels("opencode", {
|
|
21
|
-
command: runtimeConfig.runtimeCommand,
|
|
22
|
-
cwd: runtimeConfig.runtimeCwd,
|
|
23
|
-
env: runtimeConfig.runtimeEnv
|
|
24
|
-
});
|
|
25
|
-
if (configured && discovered.some((entry) => entry.id === configured)) {
|
|
26
|
-
return configured;
|
|
27
|
-
}
|
|
28
|
-
if (discovered.length > 0) {
|
|
29
|
-
return discovered[0]!.id;
|
|
30
|
-
}
|
|
31
|
-
} catch {
|
|
32
|
-
// Fall back to configured env default when discovery is unavailable.
|
|
33
|
-
}
|
|
34
|
-
return configured;
|
|
10
|
+
return runtimeConfig.runtimeModel?.trim() || undefined;
|
|
35
11
|
}
|
package/src/routes/agents.ts
CHANGED
|
@@ -22,6 +22,7 @@ import { sendError, sendOk } from "../http";
|
|
|
22
22
|
import {
|
|
23
23
|
normalizeRuntimeConfig,
|
|
24
24
|
parseRuntimeConfigFromAgentRow,
|
|
25
|
+
resolveRuntimeModelForProvider,
|
|
25
26
|
requiresRuntimeCwd,
|
|
26
27
|
runtimeConfigToDb,
|
|
27
28
|
runtimeConfigToStateBlobPatch
|
|
@@ -70,6 +71,7 @@ const runtimePreflightSchema = z.object({
|
|
|
70
71
|
"codex",
|
|
71
72
|
"cursor",
|
|
72
73
|
"opencode",
|
|
74
|
+
"gemini_cli",
|
|
73
75
|
"openai_api",
|
|
74
76
|
"anthropic_api",
|
|
75
77
|
"http",
|
|
@@ -122,6 +124,17 @@ function toAgentResponse(agent: Record<string, unknown>) {
|
|
|
122
124
|
};
|
|
123
125
|
}
|
|
124
126
|
|
|
127
|
+
function providerRequiresNamedModel(providerType: string) {
|
|
128
|
+
return providerType !== "http" && providerType !== "shell";
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function ensureNamedRuntimeModel(providerType: string, runtimeModel: string | undefined) {
|
|
132
|
+
if (!providerRequiresNamedModel(providerType)) {
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
return hasText(runtimeModel);
|
|
136
|
+
}
|
|
137
|
+
|
|
125
138
|
export function createAgentsRouter(ctx: AppContext) {
|
|
126
139
|
const router = Router();
|
|
127
140
|
router.use(requireCompanyScope);
|
|
@@ -159,6 +172,7 @@ export function createAgentsRouter(ctx: AppContext) {
|
|
|
159
172
|
| "codex"
|
|
160
173
|
| "cursor"
|
|
161
174
|
| "opencode"
|
|
175
|
+
| "gemini_cli"
|
|
162
176
|
| "openai_api"
|
|
163
177
|
| "anthropic_api"
|
|
164
178
|
| "http"
|
|
@@ -265,6 +279,10 @@ export function createAgentsRouter(ctx: AppContext) {
|
|
|
265
279
|
defaultRuntimeCwd
|
|
266
280
|
});
|
|
267
281
|
runtimeConfig.runtimeModel = await resolveOpencodeRuntimeModel(parsed.data.providerType, runtimeConfig);
|
|
282
|
+
runtimeConfig.runtimeModel = resolveRuntimeModelForProvider(parsed.data.providerType, runtimeConfig.runtimeModel);
|
|
283
|
+
if (!ensureNamedRuntimeModel(parsed.data.providerType, runtimeConfig.runtimeModel)) {
|
|
284
|
+
return sendError(res, "A named runtime model is required for this provider.", 422);
|
|
285
|
+
}
|
|
268
286
|
if (requiresRuntimeCwd(parsed.data.providerType) && !hasText(runtimeConfig.runtimeCwd)) {
|
|
269
287
|
return sendError(res, "Runtime working directory is required for this runtime provider.", 422);
|
|
270
288
|
}
|
|
@@ -397,6 +415,10 @@ export function createAgentsRouter(ctx: AppContext) {
|
|
|
397
415
|
: {})
|
|
398
416
|
};
|
|
399
417
|
nextRuntime.runtimeModel = await resolveOpencodeRuntimeModel(effectiveProviderType, nextRuntime);
|
|
418
|
+
nextRuntime.runtimeModel = resolveRuntimeModelForProvider(effectiveProviderType, nextRuntime.runtimeModel);
|
|
419
|
+
if (!ensureNamedRuntimeModel(effectiveProviderType, nextRuntime.runtimeModel)) {
|
|
420
|
+
return sendError(res, "A named runtime model is required for this provider.", 422);
|
|
421
|
+
}
|
|
400
422
|
if (!nextRuntime.runtimeCwd && defaultRuntimeCwd) {
|
|
401
423
|
nextRuntime.runtimeCwd = defaultRuntimeCwd;
|
|
402
424
|
}
|
package/src/routes/companies.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { z } from "zod";
|
|
|
3
3
|
import { createCompany, deleteCompany, listCompanies, updateCompany } from "bopodev-db";
|
|
4
4
|
import type { AppContext } from "../context";
|
|
5
5
|
import { sendError, sendOk } from "../http";
|
|
6
|
+
import { ensureCompanyModelPricingDefaults } from "../services/model-pricing";
|
|
6
7
|
import { ensureCompanyBuiltinPluginDefaults } from "../services/plugin-runtime";
|
|
7
8
|
|
|
8
9
|
const createCompanySchema = z.object({
|
|
@@ -32,6 +33,7 @@ export function createCompaniesRouter(ctx: AppContext) {
|
|
|
32
33
|
}
|
|
33
34
|
const company = await createCompany(ctx.db, parsed.data);
|
|
34
35
|
await ensureCompanyBuiltinPluginDefaults(ctx.db, company.id);
|
|
36
|
+
await ensureCompanyModelPricingDefaults(ctx.db, company.id);
|
|
35
37
|
return sendOk(res, company);
|
|
36
38
|
});
|
|
37
39
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Router } from "express";
|
|
2
|
+
import { z } from "zod";
|
|
2
3
|
import {
|
|
3
4
|
getHeartbeatRun,
|
|
4
5
|
listAgents,
|
|
@@ -6,7 +7,9 @@ import {
|
|
|
6
7
|
listCostEntries,
|
|
7
8
|
listHeartbeatRunMessages,
|
|
8
9
|
listHeartbeatRuns,
|
|
9
|
-
|
|
10
|
+
listModelPricing,
|
|
11
|
+
listPluginRuns,
|
|
12
|
+
upsertModelPricing
|
|
10
13
|
} from "bopodev-db";
|
|
11
14
|
import type { AppContext } from "../context";
|
|
12
15
|
import { sendError, sendOk } from "../http";
|
|
@@ -39,6 +42,52 @@ export function createObservabilityRouter(ctx: AppContext) {
|
|
|
39
42
|
);
|
|
40
43
|
});
|
|
41
44
|
|
|
45
|
+
const modelPricingUpdateSchema = z.object({
|
|
46
|
+
providerType: z.string().min(1),
|
|
47
|
+
modelId: z.string().min(1),
|
|
48
|
+
displayName: z.string().min(1).optional(),
|
|
49
|
+
inputUsdPer1M: z.number().min(0),
|
|
50
|
+
outputUsdPer1M: z.number().min(0),
|
|
51
|
+
currency: z.string().min(1).optional()
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
router.get("/models/pricing", async (req, res) => {
|
|
55
|
+
const rows = await listModelPricing(ctx.db, req.companyId!);
|
|
56
|
+
return sendOk(
|
|
57
|
+
res,
|
|
58
|
+
rows.map((row) => ({
|
|
59
|
+
companyId: row.companyId,
|
|
60
|
+
providerType: row.providerType,
|
|
61
|
+
modelId: row.modelId,
|
|
62
|
+
displayName: row.displayName,
|
|
63
|
+
inputUsdPer1M: typeof row.inputUsdPer1M === "number" ? row.inputUsdPer1M : Number(row.inputUsdPer1M ?? 0),
|
|
64
|
+
outputUsdPer1M: typeof row.outputUsdPer1M === "number" ? row.outputUsdPer1M : Number(row.outputUsdPer1M ?? 0),
|
|
65
|
+
currency: row.currency,
|
|
66
|
+
updatedAt: row.updatedAt?.toISOString?.() ?? null,
|
|
67
|
+
updatedBy: row.updatedBy ?? null
|
|
68
|
+
}))
|
|
69
|
+
);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
router.put("/models/pricing", async (req, res) => {
|
|
73
|
+
const parsed = modelPricingUpdateSchema.safeParse(req.body);
|
|
74
|
+
if (!parsed.success) {
|
|
75
|
+
return sendError(res, parsed.error.message, 422);
|
|
76
|
+
}
|
|
77
|
+
const payload = parsed.data;
|
|
78
|
+
await upsertModelPricing(ctx.db, {
|
|
79
|
+
companyId: req.companyId!,
|
|
80
|
+
providerType: payload.providerType,
|
|
81
|
+
modelId: payload.modelId,
|
|
82
|
+
displayName: payload.displayName ?? null,
|
|
83
|
+
inputUsdPer1M: payload.inputUsdPer1M.toFixed(6),
|
|
84
|
+
outputUsdPer1M: payload.outputUsdPer1M.toFixed(6),
|
|
85
|
+
currency: payload.currency ?? "USD",
|
|
86
|
+
updatedBy: req.actor?.id ?? null
|
|
87
|
+
});
|
|
88
|
+
return sendOk(res, { ok: true });
|
|
89
|
+
});
|
|
90
|
+
|
|
42
91
|
router.get("/heartbeats", async (req, res) => {
|
|
43
92
|
const companyId = req.companyId!;
|
|
44
93
|
const rawLimit = Number(req.query.limit ?? 100);
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
} from "bopodev-db";
|
|
18
18
|
import { normalizeRuntimeConfig, runtimeConfigToDb, runtimeConfigToStateBlobPatch } from "../lib/agent-config";
|
|
19
19
|
import { resolveDefaultRuntimeCwdForCompany } from "../lib/workspace-policy";
|
|
20
|
+
import { ensureCompanyModelPricingDefaults } from "../services/model-pricing";
|
|
20
21
|
|
|
21
22
|
export interface OnboardSeedSummary {
|
|
22
23
|
companyId: string;
|
|
@@ -30,7 +31,7 @@ export interface OnboardSeedSummary {
|
|
|
30
31
|
const DEFAULT_COMPANY_NAME_ENV = "BOPO_DEFAULT_COMPANY_NAME";
|
|
31
32
|
const DEFAULT_COMPANY_ID_ENV = "BOPO_DEFAULT_COMPANY_ID";
|
|
32
33
|
const DEFAULT_AGENT_PROVIDER_ENV = "BOPO_DEFAULT_AGENT_PROVIDER";
|
|
33
|
-
type AgentProvider = "codex" | "claude_code" | "cursor" | "opencode" | "openai_api" | "anthropic_api" | "shell";
|
|
34
|
+
type AgentProvider = "codex" | "claude_code" | "cursor" | "gemini_cli" | "opencode" | "openai_api" | "anthropic_api" | "shell";
|
|
34
35
|
const CEO_BOOTSTRAP_SUMMARY = "ceo bootstrap heartbeat";
|
|
35
36
|
const STARTUP_PROJECT_NAME = "Leadership Setup";
|
|
36
37
|
const CEO_STARTUP_TASK_TITLE = "Set up CEO operating files and hire founding engineer";
|
|
@@ -135,6 +136,7 @@ export async function ensureOnboardingSeed(input: {
|
|
|
135
136
|
ceoId
|
|
136
137
|
});
|
|
137
138
|
}
|
|
139
|
+
await ensureCompanyModelPricingDefaults(db, companyId);
|
|
138
140
|
|
|
139
141
|
return {
|
|
140
142
|
companyId,
|
|
@@ -259,6 +261,7 @@ function parseAgentProvider(value: unknown): AgentProvider | null {
|
|
|
259
261
|
value === "codex" ||
|
|
260
262
|
value === "claude_code" ||
|
|
261
263
|
value === "cursor" ||
|
|
264
|
+
value === "gemini_cli" ||
|
|
262
265
|
value === "opencode" ||
|
|
263
266
|
value === "openai_api" ||
|
|
264
267
|
value === "anthropic_api" ||
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
} from "bopodev-db";
|
|
19
19
|
import {
|
|
20
20
|
normalizeRuntimeConfig,
|
|
21
|
+
resolveRuntimeModelForProvider,
|
|
21
22
|
requiresRuntimeCwd,
|
|
22
23
|
runtimeConfigToDb,
|
|
23
24
|
runtimeConfigToStateBlobPatch
|
|
@@ -195,6 +196,10 @@ async function applyApprovalAction(db: BopoDb, companyId: string, action: string
|
|
|
195
196
|
defaultRuntimeCwd
|
|
196
197
|
});
|
|
197
198
|
runtimeConfig.runtimeModel = await resolveOpencodeRuntimeModel(parsed.data.providerType, runtimeConfig);
|
|
199
|
+
runtimeConfig.runtimeModel = resolveRuntimeModelForProvider(parsed.data.providerType, runtimeConfig.runtimeModel);
|
|
200
|
+
if (providerRequiresNamedModel(parsed.data.providerType) && !hasText(runtimeConfig.runtimeModel)) {
|
|
201
|
+
throw new GovernanceError("Approval payload for agent hiring must include a named runtime model.");
|
|
202
|
+
}
|
|
198
203
|
if (requiresRuntimeCwd(parsed.data.providerType) && !hasText(runtimeConfig.runtimeCwd)) {
|
|
199
204
|
throw new GovernanceError("Approval payload for agent hiring is missing runtime working directory.");
|
|
200
205
|
}
|
|
@@ -348,6 +353,10 @@ async function applyApprovalAction(db: BopoDb, companyId: string, action: string
|
|
|
348
353
|
throw new GovernanceError(`Unsupported approval action: ${action}`);
|
|
349
354
|
}
|
|
350
355
|
|
|
356
|
+
function providerRequiresNamedModel(providerType: string) {
|
|
357
|
+
return providerType !== "http" && providerType !== "shell";
|
|
358
|
+
}
|
|
359
|
+
|
|
351
360
|
function parsePayload(payloadJson: string) {
|
|
352
361
|
try {
|
|
353
362
|
const parsed = JSON.parse(payloadJson) as unknown;
|
|
@@ -32,6 +32,7 @@ import { createHeartbeatRunsRealtimeEvent } from "../realtime/heartbeat-runs";
|
|
|
32
32
|
import { publishOfficeOccupantForAgent } from "../realtime/office-space";
|
|
33
33
|
import { checkAgentBudget } from "./budget-service";
|
|
34
34
|
import { appendDurableFact, loadAgentMemoryContext, persistHeartbeatMemory } from "./memory-file-service";
|
|
35
|
+
import { calculateModelPricedUsdCost } from "./model-pricing";
|
|
35
36
|
import { runPluginHook } from "./plugin-runtime";
|
|
36
37
|
|
|
37
38
|
type HeartbeatRunTrigger = "manual" | "scheduler";
|
|
@@ -41,6 +42,7 @@ type HeartbeatProviderType =
|
|
|
41
42
|
| "codex"
|
|
42
43
|
| "cursor"
|
|
43
44
|
| "opencode"
|
|
45
|
+
| "gemini_cli"
|
|
44
46
|
| "openai_api"
|
|
45
47
|
| "anthropic_api"
|
|
46
48
|
| "http"
|
|
@@ -373,6 +375,8 @@ export async function runHeartbeatForAgent(
|
|
|
373
375
|
let memoryContext: HeartbeatContext["memoryContext"] | undefined;
|
|
374
376
|
let stateParseError: string | null = null;
|
|
375
377
|
let runtimeLaunchSummary: ReturnType<typeof summarizeRuntimeLaunch> | null = null;
|
|
378
|
+
let primaryIssueId: string | null = null;
|
|
379
|
+
let primaryProjectId: string | null = null;
|
|
376
380
|
let transcriptSequence = 0;
|
|
377
381
|
let transcriptWriteQueue = Promise.resolve();
|
|
378
382
|
let transcriptLiveCount = 0;
|
|
@@ -499,6 +503,8 @@ export async function runHeartbeatForAgent(
|
|
|
499
503
|
});
|
|
500
504
|
const workItems = await claimIssuesForAgent(db, companyId, agentId, runId);
|
|
501
505
|
issueIds = workItems.map((item) => item.id);
|
|
506
|
+
primaryIssueId = workItems[0]?.id ?? null;
|
|
507
|
+
primaryProjectId = workItems[0]?.project_id ?? null;
|
|
502
508
|
await runPluginHook(db, {
|
|
503
509
|
hook: "afterClaim",
|
|
504
510
|
context: {
|
|
@@ -787,6 +793,27 @@ export async function runHeartbeatForAgent(
|
|
|
787
793
|
}
|
|
788
794
|
emitCanonicalResultEvent(executionSummary, "completed");
|
|
789
795
|
executionTrace = execution.trace ?? null;
|
|
796
|
+
const runtimeModelId = resolveRuntimeModelId({
|
|
797
|
+
runtimeModel: persistedRuntime.runtimeModel,
|
|
798
|
+
stateBlob: agent.stateBlob
|
|
799
|
+
});
|
|
800
|
+
const effectivePricingProviderType = execution.pricingProviderType ?? agent.providerType;
|
|
801
|
+
const effectivePricingModelId = execution.pricingModelId ?? runtimeModelId;
|
|
802
|
+
const costDecision = await appendFinishedRunCostEntry({
|
|
803
|
+
db,
|
|
804
|
+
companyId,
|
|
805
|
+
providerType: agent.providerType,
|
|
806
|
+
runtimeModelId: effectivePricingModelId ?? runtimeModelId,
|
|
807
|
+
pricingProviderType: effectivePricingProviderType,
|
|
808
|
+
pricingModelId: effectivePricingModelId,
|
|
809
|
+
tokenInput: execution.tokenInput,
|
|
810
|
+
tokenOutput: execution.tokenOutput,
|
|
811
|
+
issueId: primaryIssueId,
|
|
812
|
+
projectId: primaryProjectId,
|
|
813
|
+
agentId,
|
|
814
|
+
status: execution.status
|
|
815
|
+
});
|
|
816
|
+
const executionUsdCost = costDecision.usdCost;
|
|
790
817
|
const parsedOutcome = ExecutionOutcomeSchema.safeParse(execution.outcome);
|
|
791
818
|
executionOutcome = parsedOutcome.success ? parsedOutcome.data : null;
|
|
792
819
|
const persistedMemory = await persistHeartbeatMemory({
|
|
@@ -835,22 +862,9 @@ export async function runHeartbeatForAgent(
|
|
|
835
862
|
}
|
|
836
863
|
}
|
|
837
864
|
|
|
838
|
-
if (execution.tokenInput > 0 || execution.tokenOutput > 0 || execution.usdCost > 0) {
|
|
839
|
-
await appendCost(db, {
|
|
840
|
-
companyId,
|
|
841
|
-
providerType: agent.providerType,
|
|
842
|
-
tokenInput: execution.tokenInput,
|
|
843
|
-
tokenOutput: execution.tokenOutput,
|
|
844
|
-
usdCost: execution.usdCost.toFixed(6),
|
|
845
|
-
issueId: workItems[0]?.id ?? null,
|
|
846
|
-
projectId: workItems[0]?.project_id ?? null,
|
|
847
|
-
agentId
|
|
848
|
-
});
|
|
849
|
-
}
|
|
850
|
-
|
|
851
865
|
if (
|
|
852
866
|
execution.nextState ||
|
|
853
|
-
|
|
867
|
+
executionUsdCost > 0 ||
|
|
854
868
|
execution.tokenInput > 0 ||
|
|
855
869
|
execution.tokenOutput > 0 ||
|
|
856
870
|
execution.status !== "skipped"
|
|
@@ -859,7 +873,8 @@ export async function runHeartbeatForAgent(
|
|
|
859
873
|
.update(agents)
|
|
860
874
|
.set({
|
|
861
875
|
stateBlob: JSON.stringify(execution.nextState ?? state),
|
|
862
|
-
|
|
876
|
+
runtimeModel: effectivePricingModelId ?? persistedRuntime.runtimeModel ?? null,
|
|
877
|
+
usedBudgetUsd: sql`${agents.usedBudgetUsd} + ${executionUsdCost}`,
|
|
863
878
|
tokenUsage: sql`${agents.tokenUsage} + ${execution.tokenInput + execution.tokenOutput}`,
|
|
864
879
|
updatedAt: new Date()
|
|
865
880
|
})
|
|
@@ -870,7 +885,7 @@ export async function runHeartbeatForAgent(
|
|
|
870
885
|
summary: execution.summary,
|
|
871
886
|
tokenInput: execution.tokenInput,
|
|
872
887
|
tokenOutput: execution.tokenOutput,
|
|
873
|
-
usdCost:
|
|
888
|
+
usdCost: executionUsdCost,
|
|
874
889
|
trace: executionTrace,
|
|
875
890
|
outcome: executionOutcome
|
|
876
891
|
});
|
|
@@ -916,7 +931,7 @@ export async function runHeartbeatForAgent(
|
|
|
916
931
|
usage: {
|
|
917
932
|
tokenInput: execution.tokenInput,
|
|
918
933
|
tokenOutput: execution.tokenOutput,
|
|
919
|
-
usdCost:
|
|
934
|
+
usdCost: executionUsdCost
|
|
920
935
|
}
|
|
921
936
|
}
|
|
922
937
|
});
|
|
@@ -1109,6 +1124,24 @@ export async function runHeartbeatForAgent(
|
|
|
1109
1124
|
cwd: runtimeLaunchSummary.cwd ?? null
|
|
1110
1125
|
};
|
|
1111
1126
|
}
|
|
1127
|
+
const runtimeModelId = resolveRuntimeModelId({
|
|
1128
|
+
runtimeModel: persistedRuntime.runtimeModel,
|
|
1129
|
+
stateBlob: agent.stateBlob
|
|
1130
|
+
});
|
|
1131
|
+
await appendFinishedRunCostEntry({
|
|
1132
|
+
db,
|
|
1133
|
+
companyId,
|
|
1134
|
+
providerType: agent.providerType,
|
|
1135
|
+
runtimeModelId,
|
|
1136
|
+
pricingProviderType: agent.providerType,
|
|
1137
|
+
pricingModelId: runtimeModelId,
|
|
1138
|
+
tokenInput: 0,
|
|
1139
|
+
tokenOutput: 0,
|
|
1140
|
+
issueId: primaryIssueId,
|
|
1141
|
+
projectId: primaryProjectId,
|
|
1142
|
+
agentId,
|
|
1143
|
+
status: "failed"
|
|
1144
|
+
});
|
|
1112
1145
|
await db
|
|
1113
1146
|
.update(heartbeatRuns)
|
|
1114
1147
|
.set({
|
|
@@ -2122,6 +2155,7 @@ function buildHeartbeatRuntimeEnv(input: {
|
|
|
2122
2155
|
BOPODEV_RUN_ID: input.heartbeatRunId,
|
|
2123
2156
|
BOPODEV_FORCE_MANAGED_CODEX_HOME: "false",
|
|
2124
2157
|
BOPODEV_API_BASE_URL: apiBaseUrl,
|
|
2158
|
+
BOPODEV_API_URL: apiBaseUrl,
|
|
2125
2159
|
BOPODEV_ACTOR_TYPE: "agent",
|
|
2126
2160
|
BOPODEV_ACTOR_ID: input.agentId,
|
|
2127
2161
|
BOPODEV_ACTOR_COMPANIES: input.companyId,
|
|
@@ -2135,7 +2169,9 @@ function buildHeartbeatRuntimeEnv(input: {
|
|
|
2135
2169
|
}
|
|
2136
2170
|
|
|
2137
2171
|
function resolveControlPlaneApiBaseUrl() {
|
|
2138
|
-
|
|
2172
|
+
// Agent runtimes must call the control-plane API directly; do not inherit
|
|
2173
|
+
// browser-facing NEXT_PUBLIC_API_URL (can point to non-runtime endpoints).
|
|
2174
|
+
const configured = resolveControlPlaneProcessEnv("API_BASE_URL");
|
|
2139
2175
|
return normalizeControlPlaneApiBaseUrl(configured) ?? "http://127.0.0.1:4020";
|
|
2140
2176
|
}
|
|
2141
2177
|
|
|
@@ -2248,7 +2284,8 @@ function shouldRequireControlPlanePreflight(
|
|
|
2248
2284
|
providerType === "codex" ||
|
|
2249
2285
|
providerType === "claude_code" ||
|
|
2250
2286
|
providerType === "cursor" ||
|
|
2251
|
-
providerType === "opencode"
|
|
2287
|
+
providerType === "opencode" ||
|
|
2288
|
+
providerType === "gemini_cli"
|
|
2252
2289
|
);
|
|
2253
2290
|
}
|
|
2254
2291
|
|
|
@@ -2367,6 +2404,68 @@ function resolveControlPlaneHeaders(runtimeEnv: Record<string, string>):
|
|
|
2367
2404
|
return { ok: true, headers: jsonHeadersResult.data };
|
|
2368
2405
|
}
|
|
2369
2406
|
|
|
2407
|
+
function resolveRuntimeModelId(input: { runtimeModel?: string; stateBlob?: string | null }) {
|
|
2408
|
+
const runtimeModel = input.runtimeModel?.trim();
|
|
2409
|
+
if (runtimeModel) {
|
|
2410
|
+
return runtimeModel;
|
|
2411
|
+
}
|
|
2412
|
+
if (!input.stateBlob) {
|
|
2413
|
+
return null;
|
|
2414
|
+
}
|
|
2415
|
+
try {
|
|
2416
|
+
const parsed = JSON.parse(input.stateBlob) as { runtime?: { model?: unknown } };
|
|
2417
|
+
const modelId = parsed.runtime?.model;
|
|
2418
|
+
return typeof modelId === "string" && modelId.trim().length > 0 ? modelId.trim() : null;
|
|
2419
|
+
} catch {
|
|
2420
|
+
return null;
|
|
2421
|
+
}
|
|
2422
|
+
}
|
|
2423
|
+
|
|
2424
|
+
async function appendFinishedRunCostEntry(input: {
|
|
2425
|
+
db: BopoDb;
|
|
2426
|
+
companyId: string;
|
|
2427
|
+
providerType: string;
|
|
2428
|
+
runtimeModelId: string | null;
|
|
2429
|
+
pricingProviderType?: string | null;
|
|
2430
|
+
pricingModelId?: string | null;
|
|
2431
|
+
tokenInput: number;
|
|
2432
|
+
tokenOutput: number;
|
|
2433
|
+
issueId?: string | null;
|
|
2434
|
+
projectId?: string | null;
|
|
2435
|
+
agentId?: string | null;
|
|
2436
|
+
status: "ok" | "failed" | "skipped";
|
|
2437
|
+
}) {
|
|
2438
|
+
const pricingDecision = await calculateModelPricedUsdCost({
|
|
2439
|
+
db: input.db,
|
|
2440
|
+
companyId: input.companyId,
|
|
2441
|
+
providerType: input.providerType,
|
|
2442
|
+
pricingProviderType: input.pricingProviderType ?? input.providerType,
|
|
2443
|
+
modelId: input.pricingModelId ?? input.runtimeModelId,
|
|
2444
|
+
tokenInput: input.tokenInput,
|
|
2445
|
+
tokenOutput: input.tokenOutput
|
|
2446
|
+
});
|
|
2447
|
+
|
|
2448
|
+
const shouldPersist = input.status === "ok" || input.status === "failed";
|
|
2449
|
+
if (shouldPersist) {
|
|
2450
|
+
await appendCost(input.db, {
|
|
2451
|
+
companyId: input.companyId,
|
|
2452
|
+
providerType: input.providerType,
|
|
2453
|
+
runtimeModelId: input.runtimeModelId,
|
|
2454
|
+
pricingProviderType: pricingDecision.pricingProviderType,
|
|
2455
|
+
pricingModelId: pricingDecision.pricingModelId,
|
|
2456
|
+
pricingSource: pricingDecision.pricingSource,
|
|
2457
|
+
tokenInput: input.tokenInput,
|
|
2458
|
+
tokenOutput: input.tokenOutput,
|
|
2459
|
+
usdCost: pricingDecision.usdCost.toFixed(6),
|
|
2460
|
+
issueId: input.issueId ?? null,
|
|
2461
|
+
projectId: input.projectId ?? null,
|
|
2462
|
+
agentId: input.agentId ?? null
|
|
2463
|
+
});
|
|
2464
|
+
}
|
|
2465
|
+
|
|
2466
|
+
return pricingDecision;
|
|
2467
|
+
}
|
|
2468
|
+
|
|
2370
2469
|
function isHeartbeatDue(cronExpression: string, lastRunAt: Date | null, now: Date) {
|
|
2371
2470
|
const normalizedNow = truncateToMinute(now);
|
|
2372
2471
|
if (!matchesCronExpression(cronExpression, normalizedNow)) {
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import type { BopoDb } from "bopodev-db";
|
|
2
|
+
import { getModelPricing, upsertModelPricing } from "bopodev-db";
|
|
3
|
+
|
|
4
|
+
type SeedModelPricingRow = {
|
|
5
|
+
providerType: "openai_api" | "anthropic_api" | "gemini_api";
|
|
6
|
+
modelId: string;
|
|
7
|
+
displayName: string;
|
|
8
|
+
inputUsdPer1M: number;
|
|
9
|
+
outputUsdPer1M: number;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const OPENAI_MODEL_BASE_PRICES: Array<{
|
|
13
|
+
modelId: string;
|
|
14
|
+
displayName: string;
|
|
15
|
+
inputUsdPer1M: number;
|
|
16
|
+
outputUsdPer1M: number;
|
|
17
|
+
}> = [
|
|
18
|
+
{ modelId: "gpt-5.2", displayName: "GPT-5.2", inputUsdPer1M: 1.75, outputUsdPer1M: 14 },
|
|
19
|
+
{ modelId: "gpt-5.1", displayName: "GPT-5.1", inputUsdPer1M: 1.25, outputUsdPer1M: 10 },
|
|
20
|
+
{ modelId: "gpt-5", displayName: "GPT-5", inputUsdPer1M: 1.25, outputUsdPer1M: 10 },
|
|
21
|
+
{ modelId: "gpt-5-mini", displayName: "GPT-5 Mini", inputUsdPer1M: 0.25, outputUsdPer1M: 2 },
|
|
22
|
+
{ modelId: "gpt-5-nano", displayName: "GPT-5 Nano", inputUsdPer1M: 0.05, outputUsdPer1M: 0.4 },
|
|
23
|
+
{ modelId: "gpt-5.3-chat-latest", displayName: "GPT-5.3 Chat Latest", inputUsdPer1M: 1.75, outputUsdPer1M: 14 },
|
|
24
|
+
{ modelId: "gpt-5.2-chat-latest", displayName: "GPT-5.2 Chat Latest", inputUsdPer1M: 1.75, outputUsdPer1M: 14 },
|
|
25
|
+
{ modelId: "gpt-5.1-chat-latest", displayName: "GPT-5.1 Chat Latest", inputUsdPer1M: 1.25, outputUsdPer1M: 10 },
|
|
26
|
+
{ modelId: "gpt-5-chat-latest", displayName: "GPT-5 Chat Latest", inputUsdPer1M: 1.25, outputUsdPer1M: 10 },
|
|
27
|
+
{ modelId: "gpt-5.4", displayName: "GPT-5.4", inputUsdPer1M: 1.75, outputUsdPer1M: 14 },
|
|
28
|
+
{ modelId: "gpt-5.3-codex", displayName: "GPT-5.3 Codex", inputUsdPer1M: 1.75, outputUsdPer1M: 14 },
|
|
29
|
+
{ modelId: "gpt-5.3-codex-spark", displayName: "GPT-5.3 Codex Spark", inputUsdPer1M: 1.75, outputUsdPer1M: 14 },
|
|
30
|
+
{ modelId: "gpt-5.2-codex", displayName: "GPT-5.2 Codex", inputUsdPer1M: 1.75, outputUsdPer1M: 14 },
|
|
31
|
+
{ modelId: "gpt-5.1-codex-max", displayName: "GPT-5.1 Codex Max", inputUsdPer1M: 1.25, outputUsdPer1M: 10 },
|
|
32
|
+
{ modelId: "gpt-5.1-codex-mini", displayName: "GPT-5.1 Codex Mini", inputUsdPer1M: 0.25, outputUsdPer1M: 2 },
|
|
33
|
+
{ modelId: "gpt-5.1-codex", displayName: "GPT-5.1 Codex", inputUsdPer1M: 1.25, outputUsdPer1M: 10 },
|
|
34
|
+
{ modelId: "gpt-5-codex", displayName: "GPT-5 Codex", inputUsdPer1M: 1.25, outputUsdPer1M: 10 },
|
|
35
|
+
{ modelId: "gpt-5.2-pro", displayName: "GPT-5.2 Pro", inputUsdPer1M: 21, outputUsdPer1M: 168 },
|
|
36
|
+
{ modelId: "gpt-5-pro", displayName: "GPT-5 Pro", inputUsdPer1M: 15, outputUsdPer1M: 120 },
|
|
37
|
+
{ modelId: "gpt-4.1", displayName: "GPT-4.1", inputUsdPer1M: 2, outputUsdPer1M: 8 },
|
|
38
|
+
{ modelId: "gpt-4.1-mini", displayName: "GPT-4.1 Mini", inputUsdPer1M: 0.4, outputUsdPer1M: 1.6 },
|
|
39
|
+
{ modelId: "gpt-4.1-nano", displayName: "GPT-4.1 Nano", inputUsdPer1M: 0.1, outputUsdPer1M: 0.4 },
|
|
40
|
+
{ modelId: "gpt-4o", displayName: "GPT-4o", inputUsdPer1M: 2.5, outputUsdPer1M: 10 },
|
|
41
|
+
{ modelId: "gpt-4o-2024-05-13", displayName: "GPT-4o 2024-05-13", inputUsdPer1M: 5, outputUsdPer1M: 15 },
|
|
42
|
+
{ modelId: "gpt-4o-mini", displayName: "GPT-4o Mini", inputUsdPer1M: 0.15, outputUsdPer1M: 0.6 },
|
|
43
|
+
{ modelId: "gpt-realtime", displayName: "GPT Realtime", inputUsdPer1M: 4, outputUsdPer1M: 16 },
|
|
44
|
+
{ modelId: "gpt-realtime-1.5", displayName: "GPT Realtime 1.5", inputUsdPer1M: 4, outputUsdPer1M: 16 },
|
|
45
|
+
{ modelId: "gpt-realtime-mini", displayName: "GPT Realtime Mini", inputUsdPer1M: 0.6, outputUsdPer1M: 2.4 },
|
|
46
|
+
{ modelId: "gpt-4o-realtime-preview", displayName: "GPT-4o Realtime Preview", inputUsdPer1M: 5, outputUsdPer1M: 20 },
|
|
47
|
+
{ modelId: "gpt-4o-mini-realtime-preview", displayName: "GPT-4o Mini Realtime Preview", inputUsdPer1M: 0.6, outputUsdPer1M: 2.4 },
|
|
48
|
+
{ modelId: "gpt-audio", displayName: "GPT Audio", inputUsdPer1M: 2.5, outputUsdPer1M: 10 },
|
|
49
|
+
{ modelId: "gpt-audio-1.5", displayName: "GPT Audio 1.5", inputUsdPer1M: 2.5, outputUsdPer1M: 10 },
|
|
50
|
+
{ modelId: "gpt-audio-mini", displayName: "GPT Audio Mini", inputUsdPer1M: 0.6, outputUsdPer1M: 2.4 },
|
|
51
|
+
{ modelId: "gpt-4o-audio-preview", displayName: "GPT-4o Audio Preview", inputUsdPer1M: 2.5, outputUsdPer1M: 10 },
|
|
52
|
+
{ modelId: "gpt-4o-mini-audio-preview", displayName: "GPT-4o Mini Audio Preview", inputUsdPer1M: 0.15, outputUsdPer1M: 0.6 },
|
|
53
|
+
{ modelId: "o1", displayName: "o1", inputUsdPer1M: 15, outputUsdPer1M: 60 },
|
|
54
|
+
{ modelId: "o1-pro", displayName: "o1-pro", inputUsdPer1M: 150, outputUsdPer1M: 600 },
|
|
55
|
+
{ modelId: "o3-pro", displayName: "o3-pro", inputUsdPer1M: 20, outputUsdPer1M: 80 },
|
|
56
|
+
{ modelId: "o3", displayName: "o3", inputUsdPer1M: 2, outputUsdPer1M: 8 },
|
|
57
|
+
{ modelId: "o3-deep-research", displayName: "o3 Deep Research", inputUsdPer1M: 10, outputUsdPer1M: 40 },
|
|
58
|
+
{ modelId: "o4-mini", displayName: "o4-mini", inputUsdPer1M: 1.1, outputUsdPer1M: 4.4 },
|
|
59
|
+
{ modelId: "o4-mini-deep-research", displayName: "o4-mini Deep Research", inputUsdPer1M: 2, outputUsdPer1M: 8 },
|
|
60
|
+
{ modelId: "o3-mini", displayName: "o3-mini", inputUsdPer1M: 1.1, outputUsdPer1M: 4.4 }
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
const CLAUDE_MODEL_BASE_PRICES: Array<{
|
|
64
|
+
modelId: string;
|
|
65
|
+
displayName: string;
|
|
66
|
+
inputUsdPer1M: number;
|
|
67
|
+
outputUsdPer1M: number;
|
|
68
|
+
}> = [
|
|
69
|
+
// Runtime ids currently used in provider model selectors.
|
|
70
|
+
{ modelId: "claude-opus-4-6", displayName: "Claude Opus 4.6", inputUsdPer1M: 5, outputUsdPer1M: 25 },
|
|
71
|
+
{ modelId: "claude-sonnet-4-6", displayName: "Claude Sonnet 4.6", inputUsdPer1M: 3, outputUsdPer1M: 15 },
|
|
72
|
+
{ modelId: "claude-sonnet-4-6-1m", displayName: "Claude Sonnet 4.6 (1M context)", inputUsdPer1M: 6, outputUsdPer1M: 22.5 },
|
|
73
|
+
{ modelId: "claude-opus-4-6-1m", displayName: "Claude Opus 4.6 (1M context)", inputUsdPer1M: 10, outputUsdPer1M: 37.5 },
|
|
74
|
+
{ modelId: "claude-haiku-4-5", displayName: "Claude Haiku 4.5", inputUsdPer1M: 1, outputUsdPer1M: 5 },
|
|
75
|
+
// Legacy / alternate ids
|
|
76
|
+
{ modelId: "claude-sonnet-4-5-20250929", displayName: "Claude Sonnet 4.5", inputUsdPer1M: 3, outputUsdPer1M: 15 },
|
|
77
|
+
{ modelId: "claude-haiku-4-5-20251001", displayName: "Claude Haiku 4.5", inputUsdPer1M: 1, outputUsdPer1M: 5 },
|
|
78
|
+
{ modelId: "claude-opus-4.6", displayName: "Claude Opus 4.6", inputUsdPer1M: 5, outputUsdPer1M: 25 },
|
|
79
|
+
{ modelId: "claude-opus-4.5", displayName: "Claude Opus 4.5", inputUsdPer1M: 5, outputUsdPer1M: 25 },
|
|
80
|
+
{ modelId: "claude-opus-4.1", displayName: "Claude Opus 4.1", inputUsdPer1M: 15, outputUsdPer1M: 75 },
|
|
81
|
+
{ modelId: "claude-opus-4", displayName: "Claude Opus 4", inputUsdPer1M: 15, outputUsdPer1M: 75 },
|
|
82
|
+
{ modelId: "claude-sonnet-4.6", displayName: "Claude Sonnet 4.6", inputUsdPer1M: 3, outputUsdPer1M: 15 },
|
|
83
|
+
{ modelId: "claude-sonnet-4.5", displayName: "Claude Sonnet 4.5", inputUsdPer1M: 3, outputUsdPer1M: 15 },
|
|
84
|
+
{ modelId: "claude-sonnet-4", displayName: "Claude Sonnet 4", inputUsdPer1M: 3, outputUsdPer1M: 15 },
|
|
85
|
+
{ modelId: "claude-sonnet-3.7", displayName: "Claude Sonnet 3.7", inputUsdPer1M: 3, outputUsdPer1M: 15 },
|
|
86
|
+
{ modelId: "claude-haiku-4.5", displayName: "Claude Haiku 4.5", inputUsdPer1M: 1, outputUsdPer1M: 5 },
|
|
87
|
+
{ modelId: "claude-haiku-3.5", displayName: "Claude Haiku 3.5", inputUsdPer1M: 0.8, outputUsdPer1M: 4 },
|
|
88
|
+
{ modelId: "claude-opus-3", displayName: "Claude Opus 3", inputUsdPer1M: 15, outputUsdPer1M: 75 },
|
|
89
|
+
{ modelId: "claude-haiku-3", displayName: "Claude Haiku 3", inputUsdPer1M: 0.25, outputUsdPer1M: 1.25 }
|
|
90
|
+
];
|
|
91
|
+
|
|
92
|
+
const GEMINI_MODEL_BASE_PRICES: Array<{
|
|
93
|
+
modelId: string;
|
|
94
|
+
displayName: string;
|
|
95
|
+
inputUsdPer1M: number;
|
|
96
|
+
outputUsdPer1M: number;
|
|
97
|
+
}> = [
|
|
98
|
+
{ modelId: "gemini-3.1-flash-lite", displayName: "Gemini 3.1 Flash Lite", inputUsdPer1M: 0.25, outputUsdPer1M: 1.5 },
|
|
99
|
+
{ modelId: "gemini-3-flash", displayName: "Gemini 3 Flash", inputUsdPer1M: 0.5, outputUsdPer1M: 3 },
|
|
100
|
+
{ modelId: "gemini-3-pro", displayName: "Gemini 3 Pro", inputUsdPer1M: 2, outputUsdPer1M: 12 },
|
|
101
|
+
{ modelId: "gemini-3-pro-200k", displayName: "Gemini 3 Pro (>200k context)", inputUsdPer1M: 4, outputUsdPer1M: 18 },
|
|
102
|
+
{ modelId: "gemini-2.5-flash-lite", displayName: "Gemini 2.5 Flash Lite", inputUsdPer1M: 0.1, outputUsdPer1M: 0.4 },
|
|
103
|
+
{ modelId: "gemini-2.5-flash", displayName: "Gemini 2.5 Flash", inputUsdPer1M: 0.3, outputUsdPer1M: 2.5 },
|
|
104
|
+
{ modelId: "gemini-2.5-pro", displayName: "Gemini 2.5 Pro", inputUsdPer1M: 1.25, outputUsdPer1M: 10 }
|
|
105
|
+
];
|
|
106
|
+
|
|
107
|
+
const DEFAULT_MODEL_PRICING_ROWS: SeedModelPricingRow[] = [
|
|
108
|
+
...OPENAI_MODEL_BASE_PRICES.map((row) => ({ ...row, providerType: "openai_api" as const })),
|
|
109
|
+
...CLAUDE_MODEL_BASE_PRICES.map((row) => ({ ...row, providerType: "anthropic_api" as const })),
|
|
110
|
+
...GEMINI_MODEL_BASE_PRICES.map((row) => ({ ...row, providerType: "gemini_api" as const }))
|
|
111
|
+
];
|
|
112
|
+
|
|
113
|
+
export async function ensureCompanyModelPricingDefaults(db: BopoDb, companyId: string) {
|
|
114
|
+
for (const row of DEFAULT_MODEL_PRICING_ROWS) {
|
|
115
|
+
await upsertModelPricing(db, {
|
|
116
|
+
companyId,
|
|
117
|
+
providerType: row.providerType,
|
|
118
|
+
modelId: row.modelId,
|
|
119
|
+
displayName: row.displayName,
|
|
120
|
+
inputUsdPer1M: row.inputUsdPer1M.toFixed(6),
|
|
121
|
+
outputUsdPer1M: row.outputUsdPer1M.toFixed(6),
|
|
122
|
+
currency: "USD",
|
|
123
|
+
updatedBy: "system:onboarding-defaults"
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export async function calculateModelPricedUsdCost(input: {
|
|
129
|
+
db: BopoDb;
|
|
130
|
+
companyId: string;
|
|
131
|
+
providerType: string;
|
|
132
|
+
pricingProviderType?: string | null;
|
|
133
|
+
modelId: string | null;
|
|
134
|
+
tokenInput: number;
|
|
135
|
+
tokenOutput: number;
|
|
136
|
+
}) {
|
|
137
|
+
const normalizedProviderType = (input.pricingProviderType ?? input.providerType).trim();
|
|
138
|
+
const normalizedModelId = input.modelId?.trim() ?? "";
|
|
139
|
+
const canonicalPricingProviderType = resolveCanonicalPricingProvider(normalizedProviderType);
|
|
140
|
+
if (!normalizedModelId || !canonicalPricingProviderType) {
|
|
141
|
+
return {
|
|
142
|
+
usdCost: 0,
|
|
143
|
+
pricingSource: "missing" as const,
|
|
144
|
+
pricingProviderType: canonicalPricingProviderType,
|
|
145
|
+
pricingModelId: normalizedModelId || null
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
const pricing = await getModelPricing(input.db, {
|
|
149
|
+
companyId: input.companyId,
|
|
150
|
+
providerType: canonicalPricingProviderType,
|
|
151
|
+
modelId: normalizedModelId
|
|
152
|
+
});
|
|
153
|
+
if (!pricing) {
|
|
154
|
+
return {
|
|
155
|
+
usdCost: 0,
|
|
156
|
+
pricingSource: "missing" as const,
|
|
157
|
+
pricingProviderType: canonicalPricingProviderType,
|
|
158
|
+
pricingModelId: normalizedModelId
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
const inputUsdPer1M = Number(pricing.inputUsdPer1M ?? 0);
|
|
162
|
+
const outputUsdPer1M = Number(pricing.outputUsdPer1M ?? 0);
|
|
163
|
+
if (!Number.isFinite(inputUsdPer1M) || !Number.isFinite(outputUsdPer1M)) {
|
|
164
|
+
return {
|
|
165
|
+
usdCost: 0,
|
|
166
|
+
pricingSource: "missing" as const,
|
|
167
|
+
pricingProviderType: canonicalPricingProviderType,
|
|
168
|
+
pricingModelId: normalizedModelId
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
const normalizedTokenInput = Math.max(0, input.tokenInput);
|
|
172
|
+
const normalizedTokenOutput = Math.max(0, input.tokenOutput);
|
|
173
|
+
if (normalizedTokenInput === 0 && normalizedTokenOutput === 0) {
|
|
174
|
+
return {
|
|
175
|
+
usdCost: 0,
|
|
176
|
+
pricingSource: "missing" as const,
|
|
177
|
+
pricingProviderType: canonicalPricingProviderType,
|
|
178
|
+
pricingModelId: normalizedModelId
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
const computedUsd =
|
|
182
|
+
(normalizedTokenInput / 1_000_000) * inputUsdPer1M +
|
|
183
|
+
(normalizedTokenOutput / 1_000_000) * outputUsdPer1M;
|
|
184
|
+
return {
|
|
185
|
+
usdCost: Number.isFinite(computedUsd) ? computedUsd : 0,
|
|
186
|
+
pricingSource: Number.isFinite(computedUsd) ? ("exact" as const) : ("missing" as const),
|
|
187
|
+
pricingProviderType: canonicalPricingProviderType,
|
|
188
|
+
pricingModelId: normalizedModelId
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function resolveCanonicalPricingProvider(providerType: string | null | undefined) {
|
|
193
|
+
const normalizedProvider = providerType?.trim() ?? "";
|
|
194
|
+
if (!normalizedProvider) {
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
if (
|
|
198
|
+
normalizedProvider === "openai_api" ||
|
|
199
|
+
normalizedProvider === "anthropic_api" ||
|
|
200
|
+
normalizedProvider === "opencode" ||
|
|
201
|
+
normalizedProvider === "gemini_api"
|
|
202
|
+
) {
|
|
203
|
+
return normalizedProvider;
|
|
204
|
+
}
|
|
205
|
+
if (normalizedProvider === "codex" || normalizedProvider === "cursor") {
|
|
206
|
+
return "openai_api";
|
|
207
|
+
}
|
|
208
|
+
if (normalizedProvider === "claude_code") {
|
|
209
|
+
return "anthropic_api";
|
|
210
|
+
}
|
|
211
|
+
if (normalizedProvider === "gemini_cli") {
|
|
212
|
+
return "gemini_api";
|
|
213
|
+
}
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
|