bopodev-api 0.1.26 → 0.1.28
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/README.md +44 -0
- package/package.json +4 -4
- package/src/app.ts +1 -2
- package/src/lib/drainable-work.ts +36 -0
- package/src/lib/run-artifact-paths.ts +80 -0
- package/src/lib/workspace-policy.ts +1 -2
- package/src/pricing/anthropic.ts +23 -0
- package/src/pricing/gemini.ts +11 -0
- package/src/pricing/index.ts +29 -0
- package/src/pricing/openai.ts +47 -0
- package/src/pricing/opencode.ts +5 -0
- package/src/pricing/types.ts +8 -0
- package/src/realtime/office-space.ts +3 -1
- package/src/routes/companies.ts +0 -2
- package/src/routes/heartbeats.ts +1 -2
- package/src/routes/issues.ts +20 -2
- package/src/routes/observability.ts +3 -136
- package/src/scripts/onboard-seed.ts +1 -3
- package/src/server.ts +112 -8
- package/src/services/attention-service.ts +90 -47
- package/src/services/budget-service.ts +1 -2
- package/src/services/comment-recipient-dispatch-service.ts +39 -2
- package/src/services/governance-service.ts +3 -1
- package/src/services/heartbeat-queue-service.ts +34 -3
- package/src/services/heartbeat-service.ts +140 -26
- package/src/services/model-pricing.ts +4 -128
- package/src/worker/scheduler.ts +20 -4
package/README.md
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# `apps/api`
|
|
2
|
+
|
|
3
|
+
Express API, websocket hub, and heartbeat scheduler runtime for Bopo.
|
|
4
|
+
|
|
5
|
+
## Responsibilities
|
|
6
|
+
|
|
7
|
+
- Serve HTTP routes for planning, execution, governance, observability, plugins, and templates.
|
|
8
|
+
- Attach websocket hub on `/realtime` for company-scoped live updates.
|
|
9
|
+
- Run heartbeat scheduler/queue worker based on `BOPO_SCHEDULER_ROLE`.
|
|
10
|
+
|
|
11
|
+
## Runtime Entry Points
|
|
12
|
+
|
|
13
|
+
- `src/server.ts` - startup, env loading, realtime bootstrap, scheduler ownership.
|
|
14
|
+
- `src/app.ts` - middleware, route mounting, error handling.
|
|
15
|
+
- `src/routes/*.ts` - route groups.
|
|
16
|
+
- `src/worker/scheduler.ts` - periodic sweep orchestration.
|
|
17
|
+
|
|
18
|
+
## Local Development
|
|
19
|
+
|
|
20
|
+
From repository root (recommended):
|
|
21
|
+
|
|
22
|
+
- `pnpm dev` or `pnpm start`
|
|
23
|
+
|
|
24
|
+
From this package directly:
|
|
25
|
+
|
|
26
|
+
- `pnpm --filter bopodev-api dev`
|
|
27
|
+
- `pnpm --filter bopodev-api start`
|
|
28
|
+
- `pnpm --filter bopodev-api db:init`
|
|
29
|
+
- `pnpm --filter bopodev-api onboard:seed`
|
|
30
|
+
- `pnpm --filter bopodev-api workspaces:backfill`
|
|
31
|
+
|
|
32
|
+
Default port is `4020` (`API_PORT`/`PORT`).
|
|
33
|
+
|
|
34
|
+
## Key Route Groups
|
|
35
|
+
|
|
36
|
+
- `/auth`, `/attention`, `/companies`, `/projects`, `/issues`, `/goals`
|
|
37
|
+
- `/agents`, `/governance`, `/heartbeats`, `/observability`
|
|
38
|
+
- `/plugins`, `/templates`
|
|
39
|
+
|
|
40
|
+
## Related Docs
|
|
41
|
+
|
|
42
|
+
- `docs/developer/api-reference.md`
|
|
43
|
+
- `docs/developer/configuration-reference.md`
|
|
44
|
+
- `docs/operations/deployment.md`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bopodev-api",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.28",
|
|
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-contracts": "0.1.
|
|
20
|
+
"bopodev-agent-sdk": "0.1.28",
|
|
21
|
+
"bopodev-db": "0.1.28",
|
|
22
|
+
"bopodev-contracts": "0.1.28"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
25
|
"@types/cors": "^2.8.19",
|
package/src/app.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import cors from "cors";
|
|
2
2
|
import express from "express";
|
|
3
3
|
import type { NextFunction, Request, Response } from "express";
|
|
4
|
-
import { sql } from "
|
|
5
|
-
import { RepositoryValidationError } from "bopodev-db";
|
|
4
|
+
import { RepositoryValidationError, sql } from "bopodev-db";
|
|
6
5
|
import { nanoid } from "nanoid";
|
|
7
6
|
import type { AppContext } from "./context";
|
|
8
7
|
import { createAgentsRouter } from "./routes/agents";
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export type DrainableWorkTracker = {
|
|
2
|
+
beginShutdown: () => void;
|
|
3
|
+
isShuttingDown: () => boolean;
|
|
4
|
+
track: <T>(promise: Promise<T>) => Promise<T>;
|
|
5
|
+
drain: () => Promise<void>;
|
|
6
|
+
resetForTests: () => void;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export function createDrainableWorkTracker(): DrainableWorkTracker {
|
|
10
|
+
let shuttingDown = false;
|
|
11
|
+
const pending = new Set<Promise<unknown>>();
|
|
12
|
+
|
|
13
|
+
return {
|
|
14
|
+
beginShutdown() {
|
|
15
|
+
shuttingDown = true;
|
|
16
|
+
},
|
|
17
|
+
isShuttingDown() {
|
|
18
|
+
return shuttingDown;
|
|
19
|
+
},
|
|
20
|
+
track<T>(promise: Promise<T>) {
|
|
21
|
+
let tracked: Promise<T>;
|
|
22
|
+
tracked = promise.finally(() => {
|
|
23
|
+
pending.delete(tracked);
|
|
24
|
+
});
|
|
25
|
+
pending.add(tracked);
|
|
26
|
+
return tracked;
|
|
27
|
+
},
|
|
28
|
+
async drain() {
|
|
29
|
+
await Promise.allSettled([...pending]);
|
|
30
|
+
},
|
|
31
|
+
resetForTests() {
|
|
32
|
+
shuttingDown = false;
|
|
33
|
+
pending.clear();
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { isAbsolute, resolve } from "node:path";
|
|
2
|
+
import { isInsidePath, resolveCompanyWorkspaceRootPath } from "./instance-paths";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Resolves a run-report artifact to an absolute path under the company workspace, or null if invalid.
|
|
6
|
+
* Shared by observability download and heartbeat artifact verification.
|
|
7
|
+
*/
|
|
8
|
+
export function resolveRunArtifactAbsolutePath(companyId: string, artifact: Record<string, unknown>) {
|
|
9
|
+
const companyWorkspaceRoot = resolveCompanyWorkspaceRootPath(companyId);
|
|
10
|
+
const absolutePathRaw = normalizeAbsoluteArtifactPath(
|
|
11
|
+
typeof artifact.absolutePath === "string" ? artifact.absolutePath.trim() : ""
|
|
12
|
+
);
|
|
13
|
+
const relativePathRaw = normalizeWorkspaceRelativeArtifactPath(
|
|
14
|
+
typeof artifact.relativePath === "string"
|
|
15
|
+
? artifact.relativePath.trim()
|
|
16
|
+
: typeof artifact.path === "string"
|
|
17
|
+
? artifact.path.trim()
|
|
18
|
+
: ""
|
|
19
|
+
);
|
|
20
|
+
const candidate = relativePathRaw
|
|
21
|
+
? resolve(companyWorkspaceRoot, relativePathRaw)
|
|
22
|
+
: absolutePathRaw
|
|
23
|
+
? absolutePathRaw
|
|
24
|
+
: "";
|
|
25
|
+
if (!candidate) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
const resolved = isAbsolute(candidate) ? resolve(candidate) : resolve(companyWorkspaceRoot, candidate);
|
|
29
|
+
if (!isInsidePath(companyWorkspaceRoot, resolved)) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
return resolved;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function normalizeAbsoluteArtifactPath(value: string) {
|
|
36
|
+
const trimmed = value.trim();
|
|
37
|
+
if (!trimmed || !isAbsolute(trimmed)) {
|
|
38
|
+
return "";
|
|
39
|
+
}
|
|
40
|
+
return resolve(trimmed);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function normalizeWorkspaceRelativeArtifactPath(value: string) {
|
|
44
|
+
const trimmed = value.trim();
|
|
45
|
+
if (!trimmed) {
|
|
46
|
+
return "";
|
|
47
|
+
}
|
|
48
|
+
const unixSeparated = trimmed.replace(/\\/g, "/");
|
|
49
|
+
if (isAbsolute(unixSeparated)) {
|
|
50
|
+
return "";
|
|
51
|
+
}
|
|
52
|
+
const parts: string[] = [];
|
|
53
|
+
for (const part of unixSeparated.split("/")) {
|
|
54
|
+
if (!part || part === ".") {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (part === "..") {
|
|
58
|
+
if (parts.length > 0 && parts[parts.length - 1] !== "..") {
|
|
59
|
+
parts.pop();
|
|
60
|
+
} else {
|
|
61
|
+
parts.push(part);
|
|
62
|
+
}
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
parts.push(part);
|
|
66
|
+
}
|
|
67
|
+
const normalized = parts.join("/");
|
|
68
|
+
if (!normalized) {
|
|
69
|
+
return "";
|
|
70
|
+
}
|
|
71
|
+
const workspaceScopedMatch = normalized.match(/(?:^|\/)workspace\/([^/]+)\/(.+)$/);
|
|
72
|
+
if (!workspaceScopedMatch) {
|
|
73
|
+
return normalized;
|
|
74
|
+
}
|
|
75
|
+
const scopedRelativePath = workspaceScopedMatch[2];
|
|
76
|
+
if (!scopedRelativePath) {
|
|
77
|
+
return "";
|
|
78
|
+
}
|
|
79
|
+
return scopedRelativePath;
|
|
80
|
+
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { and, eq, inArray } from "drizzle-orm";
|
|
2
1
|
import type { BopoDb } from "bopodev-db";
|
|
3
|
-
import { projectWorkspaces, projects } from "bopodev-db";
|
|
2
|
+
import { and, eq, inArray, projectWorkspaces, projects } from "bopodev-db";
|
|
4
3
|
import {
|
|
5
4
|
assertPathInsideCompanyWorkspaceRoot,
|
|
6
5
|
isInsidePath,
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ModelPricingCatalogRow } from "./types";
|
|
2
|
+
|
|
3
|
+
export const ANTHROPIC_MODEL_PRICING: ModelPricingCatalogRow[] = [
|
|
4
|
+
{ modelId: "claude-opus-4-6", displayName: "Claude Opus 4.6", inputUsdPer1M: 5, outputUsdPer1M: 25 },
|
|
5
|
+
{ modelId: "claude-sonnet-4-6", displayName: "Claude Sonnet 4.6", inputUsdPer1M: 3, outputUsdPer1M: 15 },
|
|
6
|
+
{ modelId: "claude-sonnet-4-6-1m", displayName: "Claude Sonnet 4.6 (1M context)", inputUsdPer1M: 6, outputUsdPer1M: 22.5 },
|
|
7
|
+
{ modelId: "claude-opus-4-6-1m", displayName: "Claude Opus 4.6 (1M context)", inputUsdPer1M: 10, outputUsdPer1M: 37.5 },
|
|
8
|
+
{ modelId: "claude-haiku-4-5", displayName: "Claude Haiku 4.5", inputUsdPer1M: 1, outputUsdPer1M: 5 },
|
|
9
|
+
{ modelId: "claude-sonnet-4-5-20250929", displayName: "Claude Sonnet 4.5", inputUsdPer1M: 3, outputUsdPer1M: 15 },
|
|
10
|
+
{ modelId: "claude-haiku-4-5-20251001", displayName: "Claude Haiku 4.5", inputUsdPer1M: 1, outputUsdPer1M: 5 },
|
|
11
|
+
{ modelId: "claude-opus-4.6", displayName: "Claude Opus 4.6", inputUsdPer1M: 5, outputUsdPer1M: 25 },
|
|
12
|
+
{ modelId: "claude-opus-4.5", displayName: "Claude Opus 4.5", inputUsdPer1M: 5, outputUsdPer1M: 25 },
|
|
13
|
+
{ modelId: "claude-opus-4.1", displayName: "Claude Opus 4.1", inputUsdPer1M: 15, outputUsdPer1M: 75 },
|
|
14
|
+
{ modelId: "claude-opus-4", displayName: "Claude Opus 4", inputUsdPer1M: 15, outputUsdPer1M: 75 },
|
|
15
|
+
{ modelId: "claude-sonnet-4.6", displayName: "Claude Sonnet 4.6", inputUsdPer1M: 3, outputUsdPer1M: 15 },
|
|
16
|
+
{ modelId: "claude-sonnet-4.5", displayName: "Claude Sonnet 4.5", inputUsdPer1M: 3, outputUsdPer1M: 15 },
|
|
17
|
+
{ modelId: "claude-sonnet-4", displayName: "Claude Sonnet 4", inputUsdPer1M: 3, outputUsdPer1M: 15 },
|
|
18
|
+
{ modelId: "claude-sonnet-3.7", displayName: "Claude Sonnet 3.7", inputUsdPer1M: 3, outputUsdPer1M: 15 },
|
|
19
|
+
{ modelId: "claude-haiku-4.5", displayName: "Claude Haiku 4.5", inputUsdPer1M: 1, outputUsdPer1M: 5 },
|
|
20
|
+
{ modelId: "claude-haiku-3.5", displayName: "Claude Haiku 3.5", inputUsdPer1M: 0.8, outputUsdPer1M: 4 },
|
|
21
|
+
{ modelId: "claude-opus-3", displayName: "Claude Opus 3", inputUsdPer1M: 15, outputUsdPer1M: 75 },
|
|
22
|
+
{ modelId: "claude-haiku-3", displayName: "Claude Haiku 3", inputUsdPer1M: 0.25, outputUsdPer1M: 1.25 }
|
|
23
|
+
];
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ModelPricingCatalogRow } from "./types";
|
|
2
|
+
|
|
3
|
+
export const GEMINI_MODEL_PRICING: ModelPricingCatalogRow[] = [
|
|
4
|
+
{ modelId: "gemini-3.1-flash-lite", displayName: "Gemini 3.1 Flash Lite", inputUsdPer1M: 0.25, outputUsdPer1M: 1.5 },
|
|
5
|
+
{ modelId: "gemini-3-flash", displayName: "Gemini 3 Flash", inputUsdPer1M: 0.5, outputUsdPer1M: 3 },
|
|
6
|
+
{ modelId: "gemini-3-pro", displayName: "Gemini 3 Pro", inputUsdPer1M: 2, outputUsdPer1M: 12 },
|
|
7
|
+
{ modelId: "gemini-3-pro-200k", displayName: "Gemini 3 Pro (>200k context)", inputUsdPer1M: 4, outputUsdPer1M: 18 },
|
|
8
|
+
{ modelId: "gemini-2.5-flash-lite", displayName: "Gemini 2.5 Flash Lite", inputUsdPer1M: 0.1, outputUsdPer1M: 0.4 },
|
|
9
|
+
{ modelId: "gemini-2.5-flash", displayName: "Gemini 2.5 Flash", inputUsdPer1M: 0.3, outputUsdPer1M: 2.5 },
|
|
10
|
+
{ modelId: "gemini-2.5-pro", displayName: "Gemini 2.5 Pro", inputUsdPer1M: 1.25, outputUsdPer1M: 10 }
|
|
11
|
+
];
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { CanonicalPricingProvider, ModelPricingCatalogRow } from "./types";
|
|
2
|
+
import { OPENAI_MODEL_PRICING } from "./openai";
|
|
3
|
+
import { ANTHROPIC_MODEL_PRICING } from "./anthropic";
|
|
4
|
+
import { GEMINI_MODEL_PRICING } from "./gemini";
|
|
5
|
+
import { OPENCODE_MODEL_PRICING } from "./opencode";
|
|
6
|
+
|
|
7
|
+
const CATALOG_BY_PROVIDER: Record<CanonicalPricingProvider, ReadonlyMap<string, ModelPricingCatalogRow>> = {
|
|
8
|
+
openai_api: buildProviderCatalog(OPENAI_MODEL_PRICING),
|
|
9
|
+
anthropic_api: buildProviderCatalog(ANTHROPIC_MODEL_PRICING),
|
|
10
|
+
gemini_api: buildProviderCatalog(GEMINI_MODEL_PRICING),
|
|
11
|
+
opencode: buildProviderCatalog(OPENCODE_MODEL_PRICING)
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export function getModelPricingCatalogRow(input: {
|
|
15
|
+
providerType: CanonicalPricingProvider;
|
|
16
|
+
modelId: string;
|
|
17
|
+
}) {
|
|
18
|
+
return CATALOG_BY_PROVIDER[input.providerType].get(input.modelId.trim()) ?? null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function buildProviderCatalog(rows: ModelPricingCatalogRow[]) {
|
|
22
|
+
const normalizedRows = rows.map((row) => ({
|
|
23
|
+
...row,
|
|
24
|
+
modelId: row.modelId.trim()
|
|
25
|
+
}));
|
|
26
|
+
return new Map(normalizedRows.map((row) => [row.modelId, row]));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export * from "./types";
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { ModelPricingCatalogRow } from "./types";
|
|
2
|
+
|
|
3
|
+
export const OPENAI_MODEL_PRICING: ModelPricingCatalogRow[] = [
|
|
4
|
+
{ modelId: "gpt-5.2", displayName: "GPT-5.2", inputUsdPer1M: 1.75, outputUsdPer1M: 14 },
|
|
5
|
+
{ modelId: "gpt-5.1", displayName: "GPT-5.1", inputUsdPer1M: 1.25, outputUsdPer1M: 10 },
|
|
6
|
+
{ modelId: "gpt-5", displayName: "GPT-5", inputUsdPer1M: 1.25, outputUsdPer1M: 10 },
|
|
7
|
+
{ modelId: "gpt-5-mini", displayName: "GPT-5 Mini", inputUsdPer1M: 0.25, outputUsdPer1M: 2 },
|
|
8
|
+
{ modelId: "gpt-5-nano", displayName: "GPT-5 Nano", inputUsdPer1M: 0.05, outputUsdPer1M: 0.4 },
|
|
9
|
+
{ modelId: "gpt-5.3-chat-latest", displayName: "GPT-5.3 Chat Latest", inputUsdPer1M: 1.75, outputUsdPer1M: 14 },
|
|
10
|
+
{ modelId: "gpt-5.2-chat-latest", displayName: "GPT-5.2 Chat Latest", inputUsdPer1M: 1.75, outputUsdPer1M: 14 },
|
|
11
|
+
{ modelId: "gpt-5.1-chat-latest", displayName: "GPT-5.1 Chat Latest", inputUsdPer1M: 1.25, outputUsdPer1M: 10 },
|
|
12
|
+
{ modelId: "gpt-5-chat-latest", displayName: "GPT-5 Chat Latest", inputUsdPer1M: 1.25, outputUsdPer1M: 10 },
|
|
13
|
+
{ modelId: "gpt-5.4", displayName: "GPT-5.4", inputUsdPer1M: 1.75, outputUsdPer1M: 14 },
|
|
14
|
+
{ modelId: "gpt-5.3-codex", displayName: "GPT-5.3 Codex", inputUsdPer1M: 1.75, outputUsdPer1M: 14 },
|
|
15
|
+
{ modelId: "gpt-5.3-codex-spark", displayName: "GPT-5.3 Codex Spark", inputUsdPer1M: 1.75, outputUsdPer1M: 14 },
|
|
16
|
+
{ modelId: "gpt-5.2-codex", displayName: "GPT-5.2 Codex", inputUsdPer1M: 1.75, outputUsdPer1M: 14 },
|
|
17
|
+
{ modelId: "gpt-5.1-codex-max", displayName: "GPT-5.1 Codex Max", inputUsdPer1M: 1.25, outputUsdPer1M: 10 },
|
|
18
|
+
{ modelId: "gpt-5.1-codex-mini", displayName: "GPT-5.1 Codex Mini", inputUsdPer1M: 0.25, outputUsdPer1M: 2 },
|
|
19
|
+
{ modelId: "gpt-5.1-codex", displayName: "GPT-5.1 Codex", inputUsdPer1M: 1.25, outputUsdPer1M: 10 },
|
|
20
|
+
{ modelId: "gpt-5-codex", displayName: "GPT-5 Codex", inputUsdPer1M: 1.25, outputUsdPer1M: 10 },
|
|
21
|
+
{ modelId: "gpt-5.2-pro", displayName: "GPT-5.2 Pro", inputUsdPer1M: 21, outputUsdPer1M: 168 },
|
|
22
|
+
{ modelId: "gpt-5-pro", displayName: "GPT-5 Pro", inputUsdPer1M: 15, outputUsdPer1M: 120 },
|
|
23
|
+
{ modelId: "gpt-4.1", displayName: "GPT-4.1", inputUsdPer1M: 2, outputUsdPer1M: 8 },
|
|
24
|
+
{ modelId: "gpt-4.1-mini", displayName: "GPT-4.1 Mini", inputUsdPer1M: 0.4, outputUsdPer1M: 1.6 },
|
|
25
|
+
{ modelId: "gpt-4.1-nano", displayName: "GPT-4.1 Nano", inputUsdPer1M: 0.1, outputUsdPer1M: 0.4 },
|
|
26
|
+
{ modelId: "gpt-4o", displayName: "GPT-4o", inputUsdPer1M: 2.5, outputUsdPer1M: 10 },
|
|
27
|
+
{ modelId: "gpt-4o-2024-05-13", displayName: "GPT-4o 2024-05-13", inputUsdPer1M: 5, outputUsdPer1M: 15 },
|
|
28
|
+
{ modelId: "gpt-4o-mini", displayName: "GPT-4o Mini", inputUsdPer1M: 0.15, outputUsdPer1M: 0.6 },
|
|
29
|
+
{ modelId: "gpt-realtime", displayName: "GPT Realtime", inputUsdPer1M: 4, outputUsdPer1M: 16 },
|
|
30
|
+
{ modelId: "gpt-realtime-1.5", displayName: "GPT Realtime 1.5", inputUsdPer1M: 4, outputUsdPer1M: 16 },
|
|
31
|
+
{ modelId: "gpt-realtime-mini", displayName: "GPT Realtime Mini", inputUsdPer1M: 0.6, outputUsdPer1M: 2.4 },
|
|
32
|
+
{ modelId: "gpt-4o-realtime-preview", displayName: "GPT-4o Realtime Preview", inputUsdPer1M: 5, outputUsdPer1M: 20 },
|
|
33
|
+
{ modelId: "gpt-4o-mini-realtime-preview", displayName: "GPT-4o Mini Realtime Preview", inputUsdPer1M: 0.6, outputUsdPer1M: 2.4 },
|
|
34
|
+
{ modelId: "gpt-audio", displayName: "GPT Audio", inputUsdPer1M: 2.5, outputUsdPer1M: 10 },
|
|
35
|
+
{ modelId: "gpt-audio-1.5", displayName: "GPT Audio 1.5", inputUsdPer1M: 2.5, outputUsdPer1M: 10 },
|
|
36
|
+
{ modelId: "gpt-audio-mini", displayName: "GPT Audio Mini", inputUsdPer1M: 0.6, outputUsdPer1M: 2.4 },
|
|
37
|
+
{ modelId: "gpt-4o-audio-preview", displayName: "GPT-4o Audio Preview", inputUsdPer1M: 2.5, outputUsdPer1M: 10 },
|
|
38
|
+
{ modelId: "gpt-4o-mini-audio-preview", displayName: "GPT-4o Mini Audio Preview", inputUsdPer1M: 0.15, outputUsdPer1M: 0.6 },
|
|
39
|
+
{ modelId: "o1", displayName: "o1", inputUsdPer1M: 15, outputUsdPer1M: 60 },
|
|
40
|
+
{ modelId: "o1-pro", displayName: "o1-pro", inputUsdPer1M: 150, outputUsdPer1M: 600 },
|
|
41
|
+
{ modelId: "o3-pro", displayName: "o3-pro", inputUsdPer1M: 20, outputUsdPer1M: 80 },
|
|
42
|
+
{ modelId: "o3", displayName: "o3", inputUsdPer1M: 2, outputUsdPer1M: 8 },
|
|
43
|
+
{ modelId: "o3-deep-research", displayName: "o3 Deep Research", inputUsdPer1M: 10, outputUsdPer1M: 40 },
|
|
44
|
+
{ modelId: "o4-mini", displayName: "o4-mini", inputUsdPer1M: 1.1, outputUsdPer1M: 4.4 },
|
|
45
|
+
{ modelId: "o4-mini-deep-research", displayName: "o4-mini Deep Research", inputUsdPer1M: 2, outputUsdPer1M: 8 },
|
|
46
|
+
{ modelId: "o3-mini", displayName: "o3-mini", inputUsdPer1M: 1.1, outputUsdPer1M: 4.4 }
|
|
47
|
+
];
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { ModelPricingCatalogRow } from "./types";
|
|
2
|
+
|
|
3
|
+
// OpenCode runtime pricing is not publicly published in a stable token-price format.
|
|
4
|
+
// Keep this catalog explicit and empty until canonical rates are available.
|
|
5
|
+
export const OPENCODE_MODEL_PRICING: ModelPricingCatalogRow[] = [];
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import { and, desc, eq } from "drizzle-orm";
|
|
2
1
|
import type { OfficeOccupant, RealtimeEventEnvelope, RealtimeMessage } from "bopodev-contracts";
|
|
3
2
|
import { AGENT_ROLE_LABELS, AgentRoleKeySchema } from "bopodev-contracts";
|
|
4
3
|
import {
|
|
4
|
+
and,
|
|
5
5
|
agents,
|
|
6
6
|
approvalRequests,
|
|
7
|
+
desc,
|
|
8
|
+
eq,
|
|
7
9
|
getApprovalRequest,
|
|
8
10
|
heartbeatRuns,
|
|
9
11
|
issues,
|
package/src/routes/companies.ts
CHANGED
|
@@ -11,7 +11,6 @@ import { buildDefaultCeoBootstrapPrompt } from "../lib/ceo-bootstrap-prompt";
|
|
|
11
11
|
import { resolveOpencodeRuntimeModel } from "../lib/opencode-model";
|
|
12
12
|
import { resolveDefaultRuntimeCwdForCompany } from "../lib/workspace-policy";
|
|
13
13
|
import { canAccessCompany, requireBoardRole, requirePermission } from "../middleware/request-actor";
|
|
14
|
-
import { ensureCompanyModelPricingDefaults } from "../services/model-pricing";
|
|
15
14
|
import { ensureCompanyBuiltinPluginDefaults } from "../services/plugin-runtime";
|
|
16
15
|
import { ensureCompanyBuiltinTemplateDefaults } from "../services/template-catalog";
|
|
17
16
|
|
|
@@ -109,7 +108,6 @@ export function createCompaniesRouter(ctx: AppContext) {
|
|
|
109
108
|
});
|
|
110
109
|
await ensureCompanyBuiltinPluginDefaults(ctx.db, company.id);
|
|
111
110
|
await ensureCompanyBuiltinTemplateDefaults(ctx.db, company.id);
|
|
112
|
-
await ensureCompanyModelPricingDefaults(ctx.db, company.id);
|
|
113
111
|
return sendOk(res, company);
|
|
114
112
|
});
|
|
115
113
|
|
package/src/routes/heartbeats.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { Router } from "express";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
-
import { and, eq } from "
|
|
4
|
-
import { agents, heartbeatRuns, listHeartbeatQueueJobs } from "bopodev-db";
|
|
3
|
+
import { agents, and, eq, heartbeatRuns, listHeartbeatQueueJobs } from "bopodev-db";
|
|
5
4
|
import type { AppContext } from "../context";
|
|
6
5
|
import { sendError, sendOk } from "../http";
|
|
7
6
|
import { requireCompanyScope } from "../middleware/company-scope";
|
package/src/routes/issues.ts
CHANGED
|
@@ -1,22 +1,26 @@
|
|
|
1
1
|
import { Router } from "express";
|
|
2
2
|
import { mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
|
|
3
3
|
import { basename, extname, join, resolve } from "node:path";
|
|
4
|
-
import { and, desc, eq, inArray } from "drizzle-orm";
|
|
5
4
|
import multer from "multer";
|
|
6
5
|
import { z } from "zod";
|
|
7
|
-
import { IssueSchema } from "bopodev-contracts";
|
|
6
|
+
import { IssueDetailSchema, IssueSchema } from "bopodev-contracts";
|
|
8
7
|
import {
|
|
9
8
|
addIssueAttachment,
|
|
10
9
|
addIssueComment,
|
|
11
10
|
agents,
|
|
11
|
+
and,
|
|
12
12
|
appendActivity,
|
|
13
13
|
appendAuditEvent,
|
|
14
14
|
createIssue,
|
|
15
15
|
deleteIssueAttachment,
|
|
16
16
|
deleteIssueComment,
|
|
17
17
|
deleteIssue,
|
|
18
|
+
desc,
|
|
19
|
+
eq,
|
|
20
|
+
getIssue,
|
|
18
21
|
heartbeatRuns,
|
|
19
22
|
getIssueAttachment,
|
|
23
|
+
inArray,
|
|
20
24
|
issues,
|
|
21
25
|
listIssueAttachments,
|
|
22
26
|
listIssueActivity,
|
|
@@ -190,6 +194,20 @@ export function createIssuesRouter(ctx: AppContext) {
|
|
|
190
194
|
);
|
|
191
195
|
});
|
|
192
196
|
|
|
197
|
+
router.get("/:issueId", async (req, res) => {
|
|
198
|
+
const issueId = req.params.issueId;
|
|
199
|
+
const issueRow = await getIssue(ctx.db, req.companyId!, issueId);
|
|
200
|
+
if (!issueRow) {
|
|
201
|
+
return sendError(res, "Issue not found.", 404);
|
|
202
|
+
}
|
|
203
|
+
const base = toIssueResponse(issueRow as unknown as Record<string, unknown>);
|
|
204
|
+
const attachmentRows = await listIssueAttachments(ctx.db, req.companyId!, issueId);
|
|
205
|
+
const attachments = attachmentRows.map((row) =>
|
|
206
|
+
toIssueAttachmentResponse(row as unknown as Record<string, unknown>, issueId)
|
|
207
|
+
);
|
|
208
|
+
return sendOkValidated(res, IssueDetailSchema, { ...base, attachments }, "issues.detail");
|
|
209
|
+
});
|
|
210
|
+
|
|
193
211
|
router.post("/", async (req, res) => {
|
|
194
212
|
requirePermission("issues:write")(req, res, () => {});
|
|
195
213
|
if (res.headersSent) {
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { Router } from "express";
|
|
2
2
|
import { readFile, stat } from "node:fs/promises";
|
|
3
|
-
import { basename,
|
|
4
|
-
import { z } from "zod";
|
|
3
|
+
import { basename, resolve } from "node:path";
|
|
5
4
|
import {
|
|
6
5
|
getHeartbeatRun,
|
|
7
6
|
listCompanies,
|
|
@@ -11,15 +10,12 @@ import {
|
|
|
11
10
|
listGoals,
|
|
12
11
|
listHeartbeatRunMessages,
|
|
13
12
|
listHeartbeatRuns,
|
|
14
|
-
|
|
15
|
-
listPluginRuns,
|
|
16
|
-
upsertModelPricing
|
|
13
|
+
listPluginRuns
|
|
17
14
|
} from "bopodev-db";
|
|
18
15
|
import type { AppContext } from "../context";
|
|
19
16
|
import { sendError, sendOk } from "../http";
|
|
20
|
-
import {
|
|
17
|
+
import { resolveRunArtifactAbsolutePath } from "../lib/run-artifact-paths";
|
|
21
18
|
import { requireCompanyScope } from "../middleware/company-scope";
|
|
22
|
-
import { requirePermission } from "../middleware/request-actor";
|
|
23
19
|
import { listAgentMemoryFiles, loadAgentMemoryContext, readAgentMemoryFile } from "../services/memory-file-service";
|
|
24
20
|
|
|
25
21
|
export function createObservabilityRouter(ctx: AppContext) {
|
|
@@ -48,56 +44,6 @@ export function createObservabilityRouter(ctx: AppContext) {
|
|
|
48
44
|
);
|
|
49
45
|
});
|
|
50
46
|
|
|
51
|
-
const modelPricingUpdateSchema = z.object({
|
|
52
|
-
providerType: z.string().min(1),
|
|
53
|
-
modelId: z.string().min(1),
|
|
54
|
-
displayName: z.string().min(1).optional(),
|
|
55
|
-
inputUsdPer1M: z.number().min(0),
|
|
56
|
-
outputUsdPer1M: z.number().min(0),
|
|
57
|
-
currency: z.string().min(1).optional()
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
router.get("/models/pricing", async (req, res) => {
|
|
61
|
-
const rows = await listModelPricing(ctx.db, req.companyId!);
|
|
62
|
-
return sendOk(
|
|
63
|
-
res,
|
|
64
|
-
rows.map((row) => ({
|
|
65
|
-
companyId: row.companyId,
|
|
66
|
-
providerType: row.providerType,
|
|
67
|
-
modelId: row.modelId,
|
|
68
|
-
displayName: row.displayName,
|
|
69
|
-
inputUsdPer1M: typeof row.inputUsdPer1M === "number" ? row.inputUsdPer1M : Number(row.inputUsdPer1M ?? 0),
|
|
70
|
-
outputUsdPer1M: typeof row.outputUsdPer1M === "number" ? row.outputUsdPer1M : Number(row.outputUsdPer1M ?? 0),
|
|
71
|
-
currency: row.currency,
|
|
72
|
-
updatedAt: row.updatedAt?.toISOString?.() ?? null,
|
|
73
|
-
updatedBy: row.updatedBy ?? null
|
|
74
|
-
}))
|
|
75
|
-
);
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
router.put("/models/pricing", async (req, res) => {
|
|
79
|
-
requirePermission("observability:write")(req, res, () => {});
|
|
80
|
-
if (res.headersSent) {
|
|
81
|
-
return;
|
|
82
|
-
}
|
|
83
|
-
const parsed = modelPricingUpdateSchema.safeParse(req.body);
|
|
84
|
-
if (!parsed.success) {
|
|
85
|
-
return sendError(res, parsed.error.message, 422);
|
|
86
|
-
}
|
|
87
|
-
const payload = parsed.data;
|
|
88
|
-
await upsertModelPricing(ctx.db, {
|
|
89
|
-
companyId: req.companyId!,
|
|
90
|
-
providerType: payload.providerType,
|
|
91
|
-
modelId: payload.modelId,
|
|
92
|
-
displayName: payload.displayName ?? null,
|
|
93
|
-
inputUsdPer1M: payload.inputUsdPer1M.toFixed(6),
|
|
94
|
-
outputUsdPer1M: payload.outputUsdPer1M.toFixed(6),
|
|
95
|
-
currency: payload.currency ?? "USD",
|
|
96
|
-
updatedBy: req.actor?.id ?? null
|
|
97
|
-
});
|
|
98
|
-
return sendOk(res, { ok: true });
|
|
99
|
-
});
|
|
100
|
-
|
|
101
47
|
router.get("/heartbeats", async (req, res) => {
|
|
102
48
|
const companyId = req.companyId!;
|
|
103
49
|
const rawLimit = Number(req.query.limit ?? 100);
|
|
@@ -404,85 +350,6 @@ function toRecord(value: unknown) {
|
|
|
404
350
|
return typeof value === "object" && value !== null ? (value as Record<string, unknown>) : null;
|
|
405
351
|
}
|
|
406
352
|
|
|
407
|
-
function resolveRunArtifactAbsolutePath(companyId: string, artifact: Record<string, unknown>) {
|
|
408
|
-
const companyWorkspaceRoot = resolveCompanyWorkspaceRootPath(companyId);
|
|
409
|
-
const absolutePathRaw = normalizeAbsoluteArtifactPath(
|
|
410
|
-
typeof artifact.absolutePath === "string" ? artifact.absolutePath.trim() : ""
|
|
411
|
-
);
|
|
412
|
-
const relativePathRaw = normalizeWorkspaceRelativeArtifactPath(
|
|
413
|
-
typeof artifact.relativePath === "string"
|
|
414
|
-
? artifact.relativePath.trim()
|
|
415
|
-
: typeof artifact.path === "string"
|
|
416
|
-
? artifact.path.trim()
|
|
417
|
-
: "",
|
|
418
|
-
companyId
|
|
419
|
-
);
|
|
420
|
-
const candidate = relativePathRaw
|
|
421
|
-
? resolve(companyWorkspaceRoot, relativePathRaw)
|
|
422
|
-
: absolutePathRaw
|
|
423
|
-
? absolutePathRaw
|
|
424
|
-
: "";
|
|
425
|
-
if (!candidate) {
|
|
426
|
-
return null;
|
|
427
|
-
}
|
|
428
|
-
const resolved = isAbsolute(candidate) ? resolve(candidate) : resolve(companyWorkspaceRoot, candidate);
|
|
429
|
-
if (!isInsidePath(companyWorkspaceRoot, resolved)) {
|
|
430
|
-
return null;
|
|
431
|
-
}
|
|
432
|
-
return resolved;
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
function normalizeAbsoluteArtifactPath(value: string) {
|
|
436
|
-
const trimmed = value.trim();
|
|
437
|
-
if (!trimmed || !isAbsolute(trimmed)) {
|
|
438
|
-
return "";
|
|
439
|
-
}
|
|
440
|
-
return resolve(trimmed);
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
function normalizeWorkspaceRelativeArtifactPath(value: string, companyId: string) {
|
|
444
|
-
const trimmed = value.trim();
|
|
445
|
-
if (!trimmed) {
|
|
446
|
-
return "";
|
|
447
|
-
}
|
|
448
|
-
const unixSeparated = trimmed.replace(/\\/g, "/");
|
|
449
|
-
if (isAbsolute(unixSeparated)) {
|
|
450
|
-
return "";
|
|
451
|
-
}
|
|
452
|
-
const parts: string[] = [];
|
|
453
|
-
for (const part of unixSeparated.split("/")) {
|
|
454
|
-
if (!part || part === ".") {
|
|
455
|
-
continue;
|
|
456
|
-
}
|
|
457
|
-
if (part === "..") {
|
|
458
|
-
if (parts.length > 0 && parts[parts.length - 1] !== "..") {
|
|
459
|
-
parts.pop();
|
|
460
|
-
} else {
|
|
461
|
-
parts.push(part);
|
|
462
|
-
}
|
|
463
|
-
continue;
|
|
464
|
-
}
|
|
465
|
-
parts.push(part);
|
|
466
|
-
}
|
|
467
|
-
const normalized = parts.join("/");
|
|
468
|
-
if (!normalized) {
|
|
469
|
-
return "";
|
|
470
|
-
}
|
|
471
|
-
const workspaceScopedMatch = normalized.match(/(?:^|\/)workspace\/([^/]+)\/(.+)$/);
|
|
472
|
-
if (!workspaceScopedMatch) {
|
|
473
|
-
return normalized;
|
|
474
|
-
}
|
|
475
|
-
const scopedCompanyId = workspaceScopedMatch[1];
|
|
476
|
-
const scopedRelativePath = workspaceScopedMatch[2];
|
|
477
|
-
if (!scopedCompanyId || !scopedRelativePath) {
|
|
478
|
-
return "";
|
|
479
|
-
}
|
|
480
|
-
if (scopedCompanyId !== companyId) {
|
|
481
|
-
return "";
|
|
482
|
-
}
|
|
483
|
-
return scopedRelativePath;
|
|
484
|
-
}
|
|
485
|
-
|
|
486
353
|
function serializeRunRow(
|
|
487
354
|
run: {
|
|
488
355
|
id: string;
|
|
@@ -30,7 +30,6 @@ import {
|
|
|
30
30
|
} from "../lib/instance-paths";
|
|
31
31
|
import { buildDefaultCeoBootstrapPrompt } from "../lib/ceo-bootstrap-prompt";
|
|
32
32
|
import { resolveDefaultRuntimeCwdForCompany } from "../lib/workspace-policy";
|
|
33
|
-
import { ensureCompanyModelPricingDefaults } from "../services/model-pricing";
|
|
34
33
|
import { applyTemplateManifest } from "../services/template-apply-service";
|
|
35
34
|
import { ensureCompanyBuiltinTemplateDefaults } from "../services/template-catalog";
|
|
36
35
|
|
|
@@ -211,8 +210,6 @@ export async function ensureOnboardingSeed(input: {
|
|
|
211
210
|
templateApplied = true;
|
|
212
211
|
appliedTemplateId = template.id;
|
|
213
212
|
}
|
|
214
|
-
await ensureCompanyModelPricingDefaults(db, companyId);
|
|
215
|
-
|
|
216
213
|
return {
|
|
217
214
|
companyId,
|
|
218
215
|
companyName: resolvedCompanyName,
|
|
@@ -313,6 +310,7 @@ async function ensureCeoStartupTask(
|
|
|
313
310
|
"Stand up your leadership operating baseline before taking on additional delivery work.",
|
|
314
311
|
"",
|
|
315
312
|
`1. Create your operating folder at \`${ceoOperatingFolder}/\`.`,
|
|
313
|
+
" During heartbeats, prefer the absolute path in `$BOPODEV_AGENT_OPERATING_DIR` (set by the runtime) so files land under your agent folder even when the shell cwd is a project workspace.",
|
|
316
314
|
"2. Author these files with your own voice and responsibilities:",
|
|
317
315
|
` - \`${ceoOperatingFolder}/AGENTS.md\``,
|
|
318
316
|
` - \`${ceoOperatingFolder}/HEARTBEAT.md\``,
|