@bitkyc08/opencodex 2.11.1 → 2.12.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.
- package/README.md +1 -1
- package/bin/ocx.mjs +4 -0
- package/gui/dist/assets/index-BNVYzdn0.css +1 -0
- package/gui/dist/assets/index-Cw1_mxO8.js +70 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/command-code.ts +94 -5
- package/src/adapters/cursor/live-transport.ts +14 -1
- package/src/adapters/google-tool-schema.ts +80 -15
- package/src/adapters/google.ts +24 -7
- package/src/adapters/openai-chat.ts +233 -305
- package/src/adapters/openai-responses.ts +62 -0
- package/src/adapters/tool-catalog-nudge.ts +2 -11
- package/src/claude/outbound.ts +42 -14
- package/src/cli/claude.ts +61 -9
- package/src/cli/codex-shim-autorestore.ts +2 -0
- package/src/cli/codex-shim-readiness.ts +8 -1
- package/src/cli/config-command.ts +4 -1
- package/src/cli/doctor.ts +10 -0
- package/src/cli/help.ts +15 -0
- package/src/cli/index.ts +27 -11
- package/src/cli/lab.ts +367 -0
- package/src/cli/provider.ts +9 -1
- package/src/clients/config-export.ts +100 -3
- package/src/codex/app-server-processes.ts +11 -8
- package/src/codex/auth-api.ts +92 -28
- package/src/codex/auth-collision.ts +5 -3
- package/src/codex/catalog/metadata.ts +7 -3
- package/src/codex/catalog/provider-fetch.ts +191 -47
- package/src/codex/catalog.ts +2 -2
- package/src/codex/history-job.ts +83 -18
- package/src/codex/history-migration-guardian.ts +9 -16
- package/src/codex/history-provider.ts +178 -1
- package/src/codex/history-transition.ts +5 -5
- package/src/codex/history-worker.ts +22 -2
- package/src/codex/native-profile-store.ts +1 -1
- package/src/codex/plan.ts +15 -0
- package/src/codex/quota.ts +6 -6
- package/src/codex/routing.ts +3 -3
- package/src/codex/subagent-model-fallback.ts +246 -22
- package/src/codex/user-identity.ts +63 -12
- package/src/config.ts +265 -32
- package/src/images/loop.ts +3 -1
- package/src/images/plan.ts +23 -13
- package/src/integrations/omp-yaml-source.ts +225 -0
- package/src/integrations/ownership.ts +5 -5
- package/src/integrations/state.ts +45 -6
- package/src/integrations/writer.ts +38 -3
- package/src/lab/artifacts/sanitize.ts +586 -0
- package/src/lab/artifacts/secure-fs.ts +475 -0
- package/src/lab/artifacts/store.ts +310 -0
- package/src/lab/conformance/assertion.ts +325 -0
- package/src/lab/conformance/digest.ts +22 -0
- package/src/lab/conformance/executor.ts +741 -0
- package/src/lab/conformance/fixture-provider.ts +27 -0
- package/src/lab/conformance/fixtures/live-v1-cases.json +175 -0
- package/src/lab/conformance/fixtures/protocol-v1-cases.json +461 -0
- package/src/lab/conformance/harness-budget.ts +47 -0
- package/src/lab/conformance/index.ts +5 -0
- package/src/lab/conformance/jcs.ts +24 -0
- package/src/lab/conformance/json-pointer.ts +39 -0
- package/src/lab/conformance/manifest.ts +180 -0
- package/src/lab/conformance/mcp-stub.ts +179 -0
- package/src/lab/conformance/negative-controls.ts +160 -0
- package/src/lab/conformance/observation.ts +355 -0
- package/src/lab/conformance/runner.ts +57 -0
- package/src/lab/conformance/sse-normalize.ts +59 -0
- package/src/lab/conformance/suite-manifest.ts +78 -0
- package/src/lab/conformance/types.ts +191 -0
- package/src/lab/constants.ts +126 -0
- package/src/lab/digest.ts +64 -0
- package/src/lab/events/errors.ts +9 -0
- package/src/lab/events/limits.ts +117 -0
- package/src/lab/events/types.ts +228 -0
- package/src/lab/events/validate.ts +780 -0
- package/src/lab/index.ts +36 -0
- package/src/lab/ledger/artifact-refs.ts +127 -0
- package/src/lab/ledger/invalidation.ts +136 -0
- package/src/lab/ledger/purge.ts +241 -0
- package/src/lab/ledger/store.ts +263 -0
- package/src/lab/live/credential-lease.ts +53 -0
- package/src/lab/live/destination.ts +155 -0
- package/src/lab/live/executor.ts +311 -0
- package/src/lab/live/inert-tools.ts +56 -0
- package/src/lab/live/manifest.ts +85 -0
- package/src/lab/live/mcp-loopback.ts +57 -0
- package/src/lab/live/runner.ts +19 -0
- package/src/lab/live/sandbox.ts +61 -0
- package/src/lab/live/suite-manifest.ts +41 -0
- package/src/lab/live/transport.ts +116 -0
- package/src/lab/live/types.ts +197 -0
- package/src/lab/observe/from-conformance.ts +344 -0
- package/src/lab/observe/from-live.ts +103 -0
- package/src/lab/paths.ts +65 -0
- package/src/lab/projection/rebuild.ts +477 -0
- package/src/lab/projection/schema.ts +122 -0
- package/src/lab/projection/verdicts.ts +438 -0
- package/src/lab/projection/verification.ts +332 -0
- package/src/lab/query/catalog.ts +72 -0
- package/src/lab/query/connection.ts +107 -0
- package/src/lab/query/constants.ts +4 -0
- package/src/lab/query/cursor.ts +132 -0
- package/src/lab/query/dto-map.ts +277 -0
- package/src/lab/query/errors.ts +22 -0
- package/src/lab/query/index.ts +34 -0
- package/src/lab/query/queries.ts +444 -0
- package/src/lab/query/types.ts +266 -0
- package/src/lab/subject/behavior-fingerprint.ts +77 -0
- package/src/lab/subject/installation-salt.ts +81 -0
- package/src/lab/subject/route-subject.ts +73 -0
- package/src/lib/bounded-body.ts +104 -0
- package/src/lib/lab-live-execution-authority.ts +13 -0
- package/src/lib/lab-live-host.ts +30 -0
- package/src/lib/lab-live-pinned-sender.ts +45 -0
- package/src/lib/pinned-http.ts +36 -7
- package/src/oauth/index.ts +20 -1
- package/src/oauth/local-token-detect.ts +11 -2
- package/src/oauth/login-cli.ts +38 -4
- package/src/providers/alibaba-region-migration.ts +1 -1
- package/src/providers/codex-capacity.ts +7 -4
- package/src/providers/command-code-efforts.ts +5 -2
- package/src/providers/derive.ts +105 -0
- package/src/providers/label.ts +1 -1
- package/src/providers/openai-tiers.ts +59 -3
- package/src/providers/opencode-zen-rate-limit.ts +102 -0
- package/src/providers/quota.ts +67 -21
- package/src/providers/registry.ts +118 -5
- package/src/router.ts +15 -1
- package/src/routing/analytics.ts +8 -7
- package/src/routing/evaluator.ts +51 -5
- package/src/routing/profile.ts +10 -1
- package/src/routing/trace.ts +42 -6
- package/src/server/auth-cors.ts +15 -0
- package/src/server/background-lifecycle.ts +169 -0
- package/src/server/chat-completions.ts +18 -3
- package/src/server/claude-messages.ts +1 -0
- package/src/server/images.ts +78 -11
- package/src/server/index.ts +72 -18
- package/src/server/lifecycle.ts +6 -0
- package/src/server/live.ts +5 -1
- package/src/server/management/agent-settings-routes.ts +41 -24
- package/src/server/management/body.ts +6 -0
- package/src/server/management/config-routes.ts +51 -8
- package/src/server/management/lab-routes.ts +388 -0
- package/src/server/management/logs-usage-routes.ts +19 -1
- package/src/server/management/model-rows.ts +25 -11
- package/src/server/management/native-integration-routes.ts +3 -3
- package/src/server/management/provider-routes.ts +5 -0
- package/src/server/management/shared.ts +8 -1
- package/src/server/management/usage-summary-cache.ts +2 -0
- package/src/server/management/vision-sidecar-options.ts +125 -0
- package/src/server/management-api.ts +2 -0
- package/src/server/request-decompress.ts +106 -5
- package/src/server/responses/collaboration.ts +4 -1
- package/src/server/responses/core.ts +117 -31
- package/src/server/responses-json-events.ts +50 -12
- package/src/server/search.ts +23 -7
- package/src/server/ws-bridge.ts +3 -1
- package/src/service.ts +40 -20
- package/src/types.ts +74 -0
- package/src/update/job.ts +10 -2
- package/src/usage/cost.ts +0 -0
- package/src/usage/expected-prices.ts +9 -0
- package/src/usage/log.ts +9 -9
- package/src/usage/user-cost-overlay-reconciler.ts +313 -0
- package/src/usage/user-cost-overlays.ts +314 -0
- package/src/vision/describe.ts +2 -2
- package/src/vision/eligibility.ts +229 -0
- package/src/vision/index.ts +20 -2
- package/src/web-search/executor.ts +2 -2
- package/src/web-search/index.ts +4 -2
- package/src/web-search/loop.ts +3 -1
- package/src/web-search/parse.ts +72 -3
- package/gui/dist/assets/index-BA1dgo4q.css +0 -1
- package/gui/dist/assets/index-DDZpgzKk.js +0 -70
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CL-04 read-only Compatibility Lab management API.
|
|
3
|
+
*
|
|
4
|
+
* - GET /api/lab/status
|
|
5
|
+
* - GET /api/lab/verdicts
|
|
6
|
+
* - GET /api/lab/subjects
|
|
7
|
+
* - GET /api/lab/subjects/:subjectId
|
|
8
|
+
* - GET /api/lab/observations
|
|
9
|
+
* - GET /api/lab/events
|
|
10
|
+
* - GET /api/lab/events/:eventId
|
|
11
|
+
* - GET /api/lab/artifacts
|
|
12
|
+
* - GET /api/lab/artifacts/:digest
|
|
13
|
+
* - GET /api/lab/catalog
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
EVIDENCE_LAYERS,
|
|
18
|
+
EXECUTION_MODES,
|
|
19
|
+
EVENT_KINDS,
|
|
20
|
+
OUTCOMES,
|
|
21
|
+
VERDICTS,
|
|
22
|
+
type EvidenceLayer,
|
|
23
|
+
type ExecutionMode,
|
|
24
|
+
type LabEventKind,
|
|
25
|
+
type ObservationOutcome,
|
|
26
|
+
type CompatibilityVerdict,
|
|
27
|
+
} from "../../lab/constants";
|
|
28
|
+
import {
|
|
29
|
+
InvalidCursorError,
|
|
30
|
+
LabProjectionIncompatibleError,
|
|
31
|
+
LabProjectionUnavailableError,
|
|
32
|
+
LAB_QUERY_MAX_PAGE_SIZE,
|
|
33
|
+
queryLabArtifactByDigest,
|
|
34
|
+
queryLabArtifacts,
|
|
35
|
+
queryLabCatalogEntries,
|
|
36
|
+
queryLabEventById,
|
|
37
|
+
queryLabEvents,
|
|
38
|
+
queryLabObservations,
|
|
39
|
+
queryLabStatus,
|
|
40
|
+
queryLabSubjectById,
|
|
41
|
+
queryLabSubjects,
|
|
42
|
+
queryLabVerdicts,
|
|
43
|
+
} from "../../lab/query";
|
|
44
|
+
import { jsonResponse } from "../auth-cors";
|
|
45
|
+
import type { ManagementContext } from "./context";
|
|
46
|
+
|
|
47
|
+
function parseQueryInt(raw: string | null): number | undefined | "invalid" {
|
|
48
|
+
if (raw === null) return undefined;
|
|
49
|
+
const trimmed = raw.trim();
|
|
50
|
+
if (trimmed.length === 0) return "invalid";
|
|
51
|
+
const value = Number(trimmed);
|
|
52
|
+
return Number.isInteger(value) ? value : "invalid";
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function errorResponse(
|
|
56
|
+
code: string,
|
|
57
|
+
message: string,
|
|
58
|
+
status: number,
|
|
59
|
+
ctx: ManagementContext,
|
|
60
|
+
): Response {
|
|
61
|
+
return jsonResponse({ error: { code, message } }, status, ctx.req, ctx.config);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function projectionErrorResponse(err: unknown, ctx: ManagementContext): Response | null {
|
|
65
|
+
if (err instanceof LabProjectionUnavailableError) {
|
|
66
|
+
return errorResponse("lab_projection_unavailable", "lab projection is not available", 503, ctx);
|
|
67
|
+
}
|
|
68
|
+
if (err instanceof LabProjectionIncompatibleError) {
|
|
69
|
+
return errorResponse("lab_projection_incompatible", "lab projection schema or spec version is incompatible", 503, ctx);
|
|
70
|
+
}
|
|
71
|
+
if (err instanceof InvalidCursorError) {
|
|
72
|
+
return errorResponse("invalid_cursor", "invalid cursor", 400, ctx);
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function parseLimit(raw: string | null, ctx: ManagementContext): number | undefined | Response {
|
|
78
|
+
const parsed = raw === null ? undefined : parseQueryInt(raw);
|
|
79
|
+
if (parsed === "invalid") {
|
|
80
|
+
return errorResponse(
|
|
81
|
+
"invalid_limit",
|
|
82
|
+
`limit must be an integer from 1 to ${LAB_QUERY_MAX_PAGE_SIZE}`,
|
|
83
|
+
400,
|
|
84
|
+
ctx,
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
if (parsed !== undefined && (parsed < 1 || parsed > LAB_QUERY_MAX_PAGE_SIZE)) {
|
|
88
|
+
return errorResponse(
|
|
89
|
+
"invalid_limit",
|
|
90
|
+
`limit must be an integer from 1 to ${LAB_QUERY_MAX_PAGE_SIZE}`,
|
|
91
|
+
400,
|
|
92
|
+
ctx,
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
return parsed;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function parseRange(
|
|
99
|
+
fromRaw: string | null,
|
|
100
|
+
toRaw: string | null,
|
|
101
|
+
ctx: ManagementContext,
|
|
102
|
+
): { from?: number; to?: number } | Response {
|
|
103
|
+
const fromParsed = parseQueryInt(fromRaw);
|
|
104
|
+
if (fromParsed === "invalid") {
|
|
105
|
+
return errorResponse("invalid_range", "from must be an integer timestamp", 400, ctx);
|
|
106
|
+
}
|
|
107
|
+
const toParsed = parseQueryInt(toRaw);
|
|
108
|
+
if (toParsed === "invalid") {
|
|
109
|
+
return errorResponse("invalid_range", "to must be an integer timestamp", 400, ctx);
|
|
110
|
+
}
|
|
111
|
+
if (fromParsed !== undefined && toParsed !== undefined && fromParsed > toParsed) {
|
|
112
|
+
return errorResponse("invalid_range", "from must not be after to", 400, ctx);
|
|
113
|
+
}
|
|
114
|
+
return { from: fromParsed, to: toParsed };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function parseLayer(raw: string | null, ctx: ManagementContext): EvidenceLayer | undefined | Response {
|
|
118
|
+
if (!raw) return undefined;
|
|
119
|
+
const trimmed = raw.trim();
|
|
120
|
+
if (!EVIDENCE_LAYERS.includes(trimmed as EvidenceLayer)) {
|
|
121
|
+
return errorResponse("invalid_layer", "layer must be a supported evidence layer", 400, ctx);
|
|
122
|
+
}
|
|
123
|
+
return trimmed as EvidenceLayer;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function parseVerdict(raw: string | null, ctx: ManagementContext): CompatibilityVerdict | undefined | Response {
|
|
127
|
+
if (!raw) return undefined;
|
|
128
|
+
const trimmed = raw.trim();
|
|
129
|
+
if (!VERDICTS.includes(trimmed as CompatibilityVerdict)) {
|
|
130
|
+
return errorResponse("invalid_verdict", "verdict must be a supported compatibility verdict", 400, ctx);
|
|
131
|
+
}
|
|
132
|
+
return trimmed as CompatibilityVerdict;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function parseEventKind(raw: string | null, ctx: ManagementContext): LabEventKind | undefined | Response {
|
|
136
|
+
if (!raw) return undefined;
|
|
137
|
+
const trimmed = raw.trim();
|
|
138
|
+
if (!EVENT_KINDS.includes(trimmed as LabEventKind)) {
|
|
139
|
+
return errorResponse("invalid_event_kind", "eventKind must be a supported lab event kind", 400, ctx);
|
|
140
|
+
}
|
|
141
|
+
return trimmed as LabEventKind;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function parseOutcome(raw: string | null, ctx: ManagementContext): ObservationOutcome | undefined | Response {
|
|
145
|
+
if (!raw) return undefined;
|
|
146
|
+
const trimmed = raw.trim();
|
|
147
|
+
if (!OUTCOMES.includes(trimmed as ObservationOutcome)) {
|
|
148
|
+
return errorResponse("invalid_outcome", "outcome must be a supported observation outcome", 400, ctx);
|
|
149
|
+
}
|
|
150
|
+
return trimmed as ObservationOutcome;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function parseExecutionMode(raw: string | null, ctx: ManagementContext): ExecutionMode | undefined | Response {
|
|
154
|
+
if (!raw) return undefined;
|
|
155
|
+
const trimmed = raw.trim();
|
|
156
|
+
if (!EXECUTION_MODES.includes(trimmed as ExecutionMode)) {
|
|
157
|
+
return errorResponse("invalid_execution_mode", "executionMode must be a supported execution mode", 400, ctx);
|
|
158
|
+
}
|
|
159
|
+
return trimmed as ExecutionMode;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function rejectUnsafeId(id: string, ctx: ManagementContext): Response | null {
|
|
163
|
+
if (id.includes("/") || id.includes("\\") || id.includes("%2f") || id.includes("%5c")) {
|
|
164
|
+
return errorResponse("not_found", "unknown resource", 404, ctx);
|
|
165
|
+
}
|
|
166
|
+
if (id.length === 0 || id.length > 256) {
|
|
167
|
+
return errorResponse("not_found", "unknown resource", 404, ctx);
|
|
168
|
+
}
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function decodePathSegment(raw: string): string | null {
|
|
173
|
+
try {
|
|
174
|
+
return decodeURIComponent(raw);
|
|
175
|
+
} catch {
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function paginatedEnvelope<T>(page: { items: T[]; nextCursor?: string; hasMore: boolean }, key: string) {
|
|
181
|
+
return {
|
|
182
|
+
[key]: page.items,
|
|
183
|
+
hasMore: page.hasMore,
|
|
184
|
+
...(page.nextCursor ? { nextCursor: page.nextCursor } : {}),
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export async function handleLabRoutes(ctx: ManagementContext): Promise<Response | null> {
|
|
189
|
+
const { url, req, config } = ctx;
|
|
190
|
+
if (!url.pathname.startsWith("/api/lab")) return null;
|
|
191
|
+
if (req.method !== "GET") return null;
|
|
192
|
+
|
|
193
|
+
if (url.pathname === "/api/lab/status") {
|
|
194
|
+
return jsonResponse(queryLabStatus(), 200, req, config);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (url.pathname === "/api/lab/catalog") {
|
|
198
|
+
const layerRaw = url.searchParams.get("layer");
|
|
199
|
+
const layerParsed = layerRaw ? parseLayer(layerRaw, ctx) : undefined;
|
|
200
|
+
if (layerParsed instanceof Response) return layerParsed;
|
|
201
|
+
const suiteId = url.searchParams.get("suiteId")?.trim() || url.searchParams.get("suite")?.trim() || undefined;
|
|
202
|
+
try {
|
|
203
|
+
const scenarios = queryLabCatalogEntries({
|
|
204
|
+
layer: layerParsed,
|
|
205
|
+
suiteId,
|
|
206
|
+
});
|
|
207
|
+
return jsonResponse({ scenarios }, 200, req, config);
|
|
208
|
+
} catch (err) {
|
|
209
|
+
const mapped = projectionErrorResponse(err, ctx);
|
|
210
|
+
if (mapped) return mapped;
|
|
211
|
+
return errorResponse("server_error", "internal lab read failure", 500, ctx);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (url.pathname === "/api/lab/verdicts") {
|
|
216
|
+
const limit = parseLimit(url.searchParams.get("limit"), ctx);
|
|
217
|
+
if (limit instanceof Response) return limit;
|
|
218
|
+
const range = parseRange(url.searchParams.get("from"), url.searchParams.get("to"), ctx);
|
|
219
|
+
if (range instanceof Response) return range;
|
|
220
|
+
const layer = parseLayer(url.searchParams.get("layer"), ctx);
|
|
221
|
+
if (layer instanceof Response) return layer;
|
|
222
|
+
const verdict = parseVerdict(url.searchParams.get("verdict"), ctx);
|
|
223
|
+
if (verdict instanceof Response) return verdict;
|
|
224
|
+
try {
|
|
225
|
+
const page = queryLabVerdicts({
|
|
226
|
+
subjectId: url.searchParams.get("subjectId")?.trim() || undefined,
|
|
227
|
+
layer,
|
|
228
|
+
suiteId: url.searchParams.get("suiteId")?.trim() || undefined,
|
|
229
|
+
verdict,
|
|
230
|
+
from: range.from,
|
|
231
|
+
to: range.to,
|
|
232
|
+
}, url.searchParams.get("cursor"), limit);
|
|
233
|
+
return jsonResponse(paginatedEnvelope(page, "verdicts"), 200, req, config);
|
|
234
|
+
} catch (err) {
|
|
235
|
+
const mapped = projectionErrorResponse(err, ctx);
|
|
236
|
+
if (mapped) return mapped;
|
|
237
|
+
return errorResponse("server_error", "internal lab read failure", 500, ctx);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (url.pathname === "/api/lab/subjects") {
|
|
242
|
+
const limit = parseLimit(url.searchParams.get("limit"), ctx);
|
|
243
|
+
if (limit instanceof Response) return limit;
|
|
244
|
+
try {
|
|
245
|
+
const page = queryLabSubjects(
|
|
246
|
+
url.searchParams.get("kind")?.trim() || undefined,
|
|
247
|
+
url.searchParams.get("cursor"),
|
|
248
|
+
limit,
|
|
249
|
+
);
|
|
250
|
+
return jsonResponse(paginatedEnvelope(page, "subjects"), 200, req, config);
|
|
251
|
+
} catch (err) {
|
|
252
|
+
const mapped = projectionErrorResponse(err, ctx);
|
|
253
|
+
if (mapped) return mapped;
|
|
254
|
+
return errorResponse("server_error", "internal lab read failure", 500, ctx);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const subjectMatch = url.pathname.match(/^\/api\/lab\/subjects\/([^/]+)$/);
|
|
259
|
+
if (subjectMatch) {
|
|
260
|
+
const subjectId = decodePathSegment(subjectMatch[1]!);
|
|
261
|
+
if (subjectId === null) return errorResponse("not_found", "unknown resource", 404, ctx);
|
|
262
|
+
const unsafe = rejectUnsafeId(subjectId, ctx);
|
|
263
|
+
if (unsafe) return unsafe;
|
|
264
|
+
try {
|
|
265
|
+
const subject = queryLabSubjectById(subjectId);
|
|
266
|
+
if (!subject) return errorResponse("not_found", "unknown subject", 404, ctx);
|
|
267
|
+
return jsonResponse({ subjectId, subject }, 200, req, config);
|
|
268
|
+
} catch (err) {
|
|
269
|
+
const mapped = projectionErrorResponse(err, ctx);
|
|
270
|
+
if (mapped) return mapped;
|
|
271
|
+
return errorResponse("server_error", "internal lab read failure", 500, ctx);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (url.pathname === "/api/lab/observations") {
|
|
276
|
+
const limit = parseLimit(url.searchParams.get("limit"), ctx);
|
|
277
|
+
if (limit instanceof Response) return limit;
|
|
278
|
+
const range = parseRange(url.searchParams.get("from"), url.searchParams.get("to"), ctx);
|
|
279
|
+
if (range instanceof Response) return range;
|
|
280
|
+
const layer = parseLayer(url.searchParams.get("layer"), ctx);
|
|
281
|
+
if (layer instanceof Response) return layer;
|
|
282
|
+
const outcome = parseOutcome(url.searchParams.get("outcome"), ctx);
|
|
283
|
+
if (outcome instanceof Response) return outcome;
|
|
284
|
+
const executionMode = parseExecutionMode(url.searchParams.get("executionMode"), ctx);
|
|
285
|
+
if (executionMode instanceof Response) return executionMode;
|
|
286
|
+
try {
|
|
287
|
+
const page = queryLabObservations({
|
|
288
|
+
subjectId: url.searchParams.get("subjectId")?.trim() || undefined,
|
|
289
|
+
layer,
|
|
290
|
+
suiteId: url.searchParams.get("suiteId")?.trim() || undefined,
|
|
291
|
+
scenarioId: url.searchParams.get("scenarioId")?.trim() || undefined,
|
|
292
|
+
outcome,
|
|
293
|
+
executionMode,
|
|
294
|
+
from: range.from,
|
|
295
|
+
to: range.to,
|
|
296
|
+
}, url.searchParams.get("cursor"), limit);
|
|
297
|
+
return jsonResponse(paginatedEnvelope(page, "observations"), 200, req, config);
|
|
298
|
+
} catch (err) {
|
|
299
|
+
const mapped = projectionErrorResponse(err, ctx);
|
|
300
|
+
if (mapped) return mapped;
|
|
301
|
+
return errorResponse("server_error", "internal lab read failure", 500, ctx);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (url.pathname === "/api/lab/events") {
|
|
306
|
+
const limit = parseLimit(url.searchParams.get("limit"), ctx);
|
|
307
|
+
if (limit instanceof Response) return limit;
|
|
308
|
+
const range = parseRange(url.searchParams.get("from"), url.searchParams.get("to"), ctx);
|
|
309
|
+
if (range instanceof Response) return range;
|
|
310
|
+
const eventKind = parseEventKind(url.searchParams.get("eventKind"), ctx);
|
|
311
|
+
if (eventKind instanceof Response) return eventKind;
|
|
312
|
+
const excludedRaw = url.searchParams.get("excluded");
|
|
313
|
+
let excluded: boolean | undefined;
|
|
314
|
+
if (excludedRaw === "true") excluded = true;
|
|
315
|
+
else if (excludedRaw === "false") excluded = false;
|
|
316
|
+
try {
|
|
317
|
+
const page = queryLabEvents({
|
|
318
|
+
eventKind,
|
|
319
|
+
subjectId: url.searchParams.get("subjectId")?.trim() || undefined,
|
|
320
|
+
from: range.from,
|
|
321
|
+
to: range.to,
|
|
322
|
+
excluded,
|
|
323
|
+
}, url.searchParams.get("cursor"), limit);
|
|
324
|
+
return jsonResponse(paginatedEnvelope(page, "events"), 200, req, config);
|
|
325
|
+
} catch (err) {
|
|
326
|
+
const mapped = projectionErrorResponse(err, ctx);
|
|
327
|
+
if (mapped) return mapped;
|
|
328
|
+
return errorResponse("server_error", "internal lab read failure", 500, ctx);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const eventMatch = url.pathname.match(/^\/api\/lab\/events\/([^/]+)$/);
|
|
333
|
+
if (eventMatch) {
|
|
334
|
+
const eventId = decodePathSegment(eventMatch[1]!);
|
|
335
|
+
if (eventId === null) return errorResponse("not_found", "unknown resource", 404, ctx);
|
|
336
|
+
const unsafe = rejectUnsafeId(eventId, ctx);
|
|
337
|
+
if (unsafe) return unsafe;
|
|
338
|
+
try {
|
|
339
|
+
const event = queryLabEventById(eventId);
|
|
340
|
+
if (!event) return errorResponse("not_found", "unknown event", 404, ctx);
|
|
341
|
+
return jsonResponse({ event }, 200, req, config);
|
|
342
|
+
} catch (err) {
|
|
343
|
+
const mapped = projectionErrorResponse(err, ctx);
|
|
344
|
+
if (mapped) return mapped;
|
|
345
|
+
return errorResponse("server_error", "internal lab read failure", 500, ctx);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (url.pathname === "/api/lab/artifacts") {
|
|
350
|
+
const limit = parseLimit(url.searchParams.get("limit"), ctx);
|
|
351
|
+
if (limit instanceof Response) return limit;
|
|
352
|
+
const statusRaw = url.searchParams.get("status")?.trim();
|
|
353
|
+
const artifactClass = url.searchParams.get("artifactClass")?.trim() || undefined;
|
|
354
|
+
if (statusRaw && !["present", "corrupt", "purged_unavailable"].includes(statusRaw)) {
|
|
355
|
+
return errorResponse("invalid_status", "status must be present, corrupt, or purged_unavailable", 400, ctx);
|
|
356
|
+
}
|
|
357
|
+
try {
|
|
358
|
+
const page = queryLabArtifacts({
|
|
359
|
+
status: statusRaw as "present" | "corrupt" | "purged_unavailable" | undefined,
|
|
360
|
+
artifactClass: artifactClass as import("../../lab/constants").ArtifactClass | undefined,
|
|
361
|
+
}, url.searchParams.get("cursor"), limit);
|
|
362
|
+
return jsonResponse(paginatedEnvelope(page, "artifacts"), 200, req, config);
|
|
363
|
+
} catch (err) {
|
|
364
|
+
const mapped = projectionErrorResponse(err, ctx);
|
|
365
|
+
if (mapped) return mapped;
|
|
366
|
+
return errorResponse("server_error", "internal lab read failure", 500, ctx);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const artifactMatch = url.pathname.match(/^\/api\/lab\/artifacts\/([^/]+)$/);
|
|
371
|
+
if (artifactMatch) {
|
|
372
|
+
const digest = decodePathSegment(artifactMatch[1]!);
|
|
373
|
+
if (digest === null) return errorResponse("not_found", "unknown resource", 404, ctx);
|
|
374
|
+
const unsafe = rejectUnsafeId(digest, ctx);
|
|
375
|
+
if (unsafe) return unsafe;
|
|
376
|
+
try {
|
|
377
|
+
const artifact = queryLabArtifactByDigest(digest);
|
|
378
|
+
if (!artifact) return errorResponse("not_found", "unknown artifact", 404, ctx);
|
|
379
|
+
return jsonResponse({ artifact }, 200, req, config);
|
|
380
|
+
} catch (err) {
|
|
381
|
+
const mapped = projectionErrorResponse(err, ctx);
|
|
382
|
+
if (mapped) return mapped;
|
|
383
|
+
return errorResponse("server_error", "internal lab read failure", 500, ctx);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
return null;
|
|
388
|
+
}
|
|
@@ -70,6 +70,7 @@ import type { OcxClaudeCodeConfig, OcxConfig, OcxCustomModel, OcxProviderConfig
|
|
|
70
70
|
import { drainAndShutdown } from "../lifecycle";
|
|
71
71
|
import { filterRequestLogs, filteredRequestLogCount, getRequestLogEntries, type RequestLogEntry } from "../request-log";
|
|
72
72
|
import { estimateComboCost, estimateRequestCost, normalizeCostTokens, tokensPerSecond } from "../../usage/cost";
|
|
73
|
+
import { userCostOverlayVersion } from "../../usage/user-cost-overlays";
|
|
73
74
|
import type { PersistedUsageAttempt } from "../../usage/log";
|
|
74
75
|
import { isAllowedRequestOrigin, jsonResponse, providerManagementConfigError, publicProviderBaseUrl, safeConfigDTO } from "../auth-cors";
|
|
75
76
|
import { applySystemEnvToggle } from "../system-env";
|
|
@@ -193,10 +194,19 @@ export async function handleLogsUsageRoutes(ctx: ManagementContext): Promise<Res
|
|
|
193
194
|
const effectiveReadLimit = config.managementUsageMaxReadBytes ?? 64 * 1024 * 1024;
|
|
194
195
|
const observedRevisionKey = `${usageLogRevisionKey(currentUsageLogRevision())}\0${effectiveReadLimit}`;
|
|
195
196
|
const cached = getUsageSummaryCacheEntry(cacheKey);
|
|
196
|
-
if (cached
|
|
197
|
+
if (cached
|
|
198
|
+
&& cached.revisionKey === observedRevisionKey
|
|
199
|
+
&& cached.overlayVersion === userCostOverlayVersion()
|
|
200
|
+
&& now < cached.expiresAt) {
|
|
197
201
|
return jsonResponse(refreshedUsageSummary(cached.summary, range, now));
|
|
198
202
|
}
|
|
199
203
|
if (cached) discardUsageSummaryCacheEntry(cacheKey);
|
|
204
|
+
// Capture the overlay version BEFORE reading/computing: the cache entry
|
|
205
|
+
// must be stamped with the version the summary was priced under. Reading
|
|
206
|
+
// it again at stamp time could cache an old-price summary as current,
|
|
207
|
+
// and the next request would then accept stale pricing for the whole
|
|
208
|
+
// cache lifetime.
|
|
209
|
+
const overlayVersion = userCostOverlayVersion();
|
|
200
210
|
const snapshot = await readUsageSnapshotForManagement(effectiveReadLimit);
|
|
201
211
|
const revisionReadAt = Date.now();
|
|
202
212
|
const summary = {
|
|
@@ -206,8 +216,16 @@ export async function handleLogsUsageRoutes(ctx: ManagementContext): Promise<Res
|
|
|
206
216
|
entriesTruncated: snapshot.entriesTruncated,
|
|
207
217
|
entriesDropped: snapshot.entriesDropped,
|
|
208
218
|
};
|
|
219
|
+
if (userCostOverlayVersion() !== overlayVersion) {
|
|
220
|
+
// The overlay changed while the summary was being computed, so this
|
|
221
|
+
// summary may mix old and new prices. Serve it uncached: the next
|
|
222
|
+
// request recomputes against the settled overlay instead of caching a
|
|
223
|
+
// mixed-price entry under either version.
|
|
224
|
+
return jsonResponse(summary);
|
|
225
|
+
}
|
|
209
226
|
setUsageSummaryCacheEntry(cacheKey, {
|
|
210
227
|
revisionKey: `${usageLogRevisionKey(snapshot.revision)}\0${effectiveReadLimit}`,
|
|
228
|
+
overlayVersion,
|
|
211
229
|
expiresAt: usageSummaryExpiresAt(snapshot.entries, range, surface, now),
|
|
212
230
|
revisionReadAt,
|
|
213
231
|
summary,
|
|
@@ -9,7 +9,14 @@
|
|
|
9
9
|
* Bodies are unchanged from their previous home; only `export` was added.
|
|
10
10
|
*/
|
|
11
11
|
import type { CatalogModel } from "../../codex/catalog";
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
catalogModelSlug,
|
|
14
|
+
nativeDefaultReasoningEffort,
|
|
15
|
+
nativeInputModalities,
|
|
16
|
+
nativeModelRows,
|
|
17
|
+
nativeReasoningEfforts,
|
|
18
|
+
uniqueCatalogModelsForPublicList,
|
|
19
|
+
} from "../../codex/catalog";
|
|
13
20
|
import type { ExportModel } from "../../clients/config-export";
|
|
14
21
|
import { providerContextCap } from "../../providers/context-cap";
|
|
15
22
|
import { isVisionReasoningEffort } from "../../reasoning-effort";
|
|
@@ -42,16 +49,21 @@ export async function listManagementModelRows(config: OcxConfig): Promise<Manage
|
|
|
42
49
|
const disabled = new Set(config.disabledModels ?? []);
|
|
43
50
|
// Native GPT passthrough rows lead (provider "openai", bare-slug namespaced ids): sourced
|
|
44
51
|
// from the static supported set so a disabled model stays listed and re-enableable.
|
|
45
|
-
const native: ManagementModelRow[] = nativeModelRows(config).map(row =>
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
52
|
+
const native: ManagementModelRow[] = nativeModelRows(config).map(row => {
|
|
53
|
+
const reasoningEfforts = nativeReasoningEfforts(row.slug).filter(isVisionReasoningEffort);
|
|
54
|
+
const defaultReasoningEffort = nativeDefaultReasoningEffort(row.slug);
|
|
55
|
+
return {
|
|
56
|
+
provider: "openai",
|
|
57
|
+
id: row.slug,
|
|
58
|
+
namespaced: row.slug,
|
|
59
|
+
disabled: row.disabled,
|
|
60
|
+
native: true,
|
|
61
|
+
reasoningEfforts,
|
|
62
|
+
...(defaultReasoningEffort ? { defaultReasoningEffort } : {}),
|
|
63
|
+
inputModalities: nativeInputModalities(row.slug),
|
|
64
|
+
...(row.contextWindow !== undefined ? { contextWindow: row.contextWindow } : {}),
|
|
65
|
+
};
|
|
66
|
+
});
|
|
55
67
|
const customModels: ManagementModelRow[] = (config.customModels ?? []).map(cm => {
|
|
56
68
|
const namespaced = routedSlug(cm.provider, cm.modelId);
|
|
57
69
|
return {
|
|
@@ -102,6 +114,8 @@ export function toExportModel(row: ManagementModelRow): ExportModel {
|
|
|
102
114
|
...(row.displayName ? { displayName: row.displayName } : {}),
|
|
103
115
|
...(row.contextWindow !== undefined ? { contextWindow: row.contextWindow } : {}),
|
|
104
116
|
...(row.inputModalities ? { inputModalities: row.inputModalities } : {}),
|
|
117
|
+
...(row.reasoningEfforts ? { reasoningEfforts: row.reasoningEfforts } : {}),
|
|
118
|
+
...(row.defaultReasoningEffort ? { defaultReasoningEffort: row.defaultReasoningEffort } : {}),
|
|
105
119
|
};
|
|
106
120
|
}
|
|
107
121
|
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* 011 (Claude Code), 012 (Grok).
|
|
19
19
|
*/
|
|
20
20
|
import { loadConfig, readRuntimePort, saveConfigPreservingClaudeCode } from "../../config";
|
|
21
|
-
import { filterCatalogVisibleModels, nativeOpenAiContextWindow, visibleNativeSlugs } from "../../codex/catalog";
|
|
21
|
+
import { desktopVisibleNativeSlugs, filterCatalogVisibleModels, nativeOpenAiContextWindow, visibleNativeSlugs } from "../../codex/catalog";
|
|
22
22
|
import { inspectDesktop3pConfigLibrary, removeDesktop3pStandardPivot, writeDesktop3pConfig } from "../../claude/desktop-3p";
|
|
23
23
|
import { injectGrokConfig, stripGrokConfig, type GrokInjectModel } from "../../grok/inject";
|
|
24
24
|
import { inspectGrokConfig } from "../../grok/inspect";
|
|
@@ -656,7 +656,7 @@ async function handleClaudeDesktopToggle(ctx: ManagementContext): Promise<Respon
|
|
|
656
656
|
const runtime = (ctx.deps.readRuntimePort ?? readRuntimePort)(process.pid);
|
|
657
657
|
const result = (ctx.deps.writeDesktop3pConfig ?? writeDesktop3pConfig)(
|
|
658
658
|
runtime?.port ?? latest.port,
|
|
659
|
-
[...
|
|
659
|
+
[...desktopVisibleNativeSlugs(latest)],
|
|
660
660
|
routed,
|
|
661
661
|
latest.apiKeys?.[0]?.key,
|
|
662
662
|
"static",
|
|
@@ -696,7 +696,7 @@ export async function handleNativeIntegrationRoutes(ctx: ManagementContext): Pro
|
|
|
696
696
|
rethrowManagementBodyTooLarge(error);
|
|
697
697
|
return jsonResponse({ error: "invalid JSON body" }, 400);
|
|
698
698
|
}
|
|
699
|
-
if (typeof body.enabled !== "boolean") {
|
|
699
|
+
if (!body || typeof body !== "object" || Array.isArray(body) || typeof body.enabled !== "boolean") {
|
|
700
700
|
return jsonResponse({ error: "enabled must be a boolean" }, 400);
|
|
701
701
|
}
|
|
702
702
|
|
|
@@ -340,6 +340,11 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
340
340
|
// let the (possibly new) apiKey join the pool as the active entry.
|
|
341
341
|
const existingPool = config.providers[name]?.apiKeyPool;
|
|
342
342
|
if (existingPool && !prov.apiKeyPool) prov.apiKeyPool = existingPool;
|
|
343
|
+
// The same rule applies to user-configured price overlays: the dashboard's
|
|
344
|
+
// add/edit form does not send modelCosts, so an overwrite must not silently
|
|
345
|
+
// erase hand-edited per-model prices from Logs/Usage estimates.
|
|
346
|
+
const existingCosts = config.providers[name]?.modelCosts;
|
|
347
|
+
if (existingCosts && !prov.modelCosts) prov.modelCosts = existingCosts;
|
|
343
348
|
config.providers[name] = stripRegistryOnlyStaticHeaders(name, prov);
|
|
344
349
|
if (body.setDefault === true) config.defaultProvider = name;
|
|
345
350
|
save(config);
|
|
@@ -82,7 +82,11 @@ export type TokPerSecondResult =
|
|
|
82
82
|
| { kind: "value"; value: number; estimated: boolean }
|
|
83
83
|
| { kind: "unavailable"; reason: MetricUnavailableReason };
|
|
84
84
|
|
|
85
|
-
export type CostEstimateReason =
|
|
85
|
+
export type CostEstimateReason =
|
|
86
|
+
| "usage_estimated"
|
|
87
|
+
| "cache_detail_missing"
|
|
88
|
+
| "expected_price_overlay"
|
|
89
|
+
| "provider_cost_overlay";
|
|
86
90
|
|
|
87
91
|
export type CostResult =
|
|
88
92
|
| { kind: "value"; estimate: NonNullable<ReturnType<typeof estimateRequestCost>>; estimateReasons: CostEstimateReason[] }
|
|
@@ -123,6 +127,7 @@ export function unavailableCostReason(entry: MetricSource): MetricUnavailableRea
|
|
|
123
127
|
return "price_unmatched";
|
|
124
128
|
}
|
|
125
129
|
|
|
130
|
+
/** Display-time cost estimate for one log entry (or its attempt list), including the reasons that qualify the estimate. */
|
|
126
131
|
export function costResult(entry: MetricSource): CostResult {
|
|
127
132
|
const tier = serviceTierContext(entry);
|
|
128
133
|
const estimate = entry.attempts?.length
|
|
@@ -136,6 +141,8 @@ export function costResult(entry: MetricSource): CostResult {
|
|
|
136
141
|
&& entry.usage.cacheCreationInputTokens === undefined ? "cache_detail_missing" as const : undefined,
|
|
137
142
|
estimate.price?.source === "expected" || estimate.attempts?.some(a => a.price.source === "expected")
|
|
138
143
|
? "expected_price_overlay" as const : undefined,
|
|
144
|
+
estimate.price?.source === "user" || estimate.attempts?.some(a => a.price.source === "user")
|
|
145
|
+
? "provider_cost_overlay" as const : undefined,
|
|
139
146
|
].filter((reason): reason is CostEstimateReason => reason !== undefined);
|
|
140
147
|
return { kind: "value", estimate, estimateReasons };
|
|
141
148
|
}
|
|
@@ -10,6 +10,8 @@ export type CachedUsageSummary = UsageSummary & {
|
|
|
10
10
|
|
|
11
11
|
interface UsageSummaryCacheEntry {
|
|
12
12
|
revisionKey: string;
|
|
13
|
+
/** userCostOverlayVersion() when the summary was computed; overlay edits invalidate the entry. */
|
|
14
|
+
overlayVersion: number;
|
|
13
15
|
expiresAt: number;
|
|
14
16
|
summary: CachedUsageSummary;
|
|
15
17
|
revisionReadAt: number;
|