@bitkyc08/opencodex 2.7.20 → 2.7.21
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/gui/dist/assets/{index-BpqkH5f_.css → index-CILVKWmx.css} +1 -1
- package/gui/dist/assets/index-DYIS0tTL.js +40 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/claude/agents-inject.ts +33 -2
- package/src/claude/inbound.ts +10 -1
- package/src/server/claude-messages.ts +51 -15
- package/src/server/management-api.ts +4 -2
- package/src/server/request-log.ts +1 -0
- package/src/usage/log.ts +2 -0
- package/src/usage/summary.ts +24 -6
- package/gui/dist/assets/index-BH-RbAg4.js +0 -40
package/gui/dist/index.html
CHANGED
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
} catch (e) {}
|
|
17
17
|
})();
|
|
18
18
|
</script>
|
|
19
|
-
<script type="module" crossorigin src="/assets/index-
|
|
20
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
19
|
+
<script type="module" crossorigin src="/assets/index-DYIS0tTL.js"></script>
|
|
20
|
+
<link rel="stylesheet" crossorigin href="/assets/index-CILVKWmx.css">
|
|
21
21
|
</head>
|
|
22
22
|
<body>
|
|
23
23
|
<div id="root"></div>
|
package/package.json
CHANGED
|
@@ -15,15 +15,17 @@ import { lstatSync, mkdirSync, readdirSync, readFileSync, renameSync, unlinkSync
|
|
|
15
15
|
import { join } from "node:path";
|
|
16
16
|
import type { OcxConfig } from "../types";
|
|
17
17
|
import { claudeCodeAlias, claudeCodeNativeAlias } from "./alias";
|
|
18
|
-
import { resolveAutoContext, withOneMillionMarker } from "./context-windows";
|
|
18
|
+
import { resolveAutoContext, stripOneMillionMarker, withOneMillionMarker } from "./context-windows";
|
|
19
19
|
import { claudeConfigDir } from "./gateway-cache";
|
|
20
20
|
import { DEFAULT_SUBAGENT_MODELS } from "../config";
|
|
21
|
+
import { effectiveBlockedSkillNames, resolveInboundModel } from "./inbound";
|
|
21
22
|
|
|
22
23
|
export interface ClaudeAgentDef {
|
|
23
24
|
file: string;
|
|
24
25
|
name: string;
|
|
25
26
|
model: string;
|
|
26
27
|
description: string;
|
|
28
|
+
blockedSkills: readonly string[];
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
const OWNED_PREFIX = "ocx-";
|
|
@@ -63,6 +65,15 @@ function entryParts(entry: string): { alias: string; id: string; provider: strin
|
|
|
63
65
|
|
|
64
66
|
export function buildClaudeAgentDefs(config: OcxConfig, windows: Record<string, number>, configDir = claudeConfigDir()): ClaudeAgentDef[] {
|
|
65
67
|
const auto = resolveAutoContext(config.claudeCode);
|
|
68
|
+
const blockedSkills = effectiveBlockedSkillNames(config.claudeCode);
|
|
69
|
+
const blockedSkillsFor = (model: string): readonly string[] => {
|
|
70
|
+
const unmarked = stripOneMillionMarker(model);
|
|
71
|
+
const nativePassthrough = config.claudeCode?.nativePassthrough !== false
|
|
72
|
+
&& !unmarked.includes("/")
|
|
73
|
+
&& /^(claude|anthropic)(?:-|$)/i.test(unmarked)
|
|
74
|
+
&& resolveInboundModel(unmarked, config.claudeCode) === unmarked;
|
|
75
|
+
return nativePassthrough ? [] : blockedSkills;
|
|
76
|
+
};
|
|
66
77
|
const defs: ClaudeAgentDef[] = [];
|
|
67
78
|
const usedNames = new Set<string>();
|
|
68
79
|
const coveredModels = new Set<string>();
|
|
@@ -76,7 +87,13 @@ export function buildClaudeAgentDefs(config: OcxConfig, windows: Record<string,
|
|
|
76
87
|
let unique = name;
|
|
77
88
|
for (let i = 2; usedNames.has(unique); i++) unique = `${name}-${i}`;
|
|
78
89
|
usedNames.add(unique);
|
|
79
|
-
defs.push({
|
|
90
|
+
defs.push({
|
|
91
|
+
file: `${OWNED_PREFIX}${unique}.md`,
|
|
92
|
+
name: `${OWNED_PREFIX}${unique}`,
|
|
93
|
+
model,
|
|
94
|
+
description,
|
|
95
|
+
blockedSkills: blockedSkillsFor(model),
|
|
96
|
+
});
|
|
80
97
|
};
|
|
81
98
|
|
|
82
99
|
// Default roster applies only when the field is UNSET — an explicit [] is
|
|
@@ -100,12 +117,25 @@ export function buildClaudeAgentDefs(config: OcxConfig, windows: Record<string,
|
|
|
100
117
|
name: `${OWNED_PREFIX}self`,
|
|
101
118
|
model: marked,
|
|
102
119
|
description: `Self-clone: delegate to your default main model (${marked}), synced from the /model picker at launch. ${NO_MODEL_ARG}`,
|
|
120
|
+
blockedSkills: blockedSkillsFor(marked),
|
|
103
121
|
});
|
|
104
122
|
}
|
|
105
123
|
return defs;
|
|
106
124
|
}
|
|
107
125
|
|
|
126
|
+
function skillNameLiteral(name: string): string {
|
|
127
|
+
return JSON.stringify(name)
|
|
128
|
+
.replaceAll("`", "\\u0060")
|
|
129
|
+
.replaceAll("<", "\\u003c")
|
|
130
|
+
.replaceAll(">", "\\u003e");
|
|
131
|
+
}
|
|
132
|
+
|
|
108
133
|
function renderAgentDef(def: ClaudeAgentDef): string {
|
|
134
|
+
const blockedSkillGuard = def.blockedSkills.length === 0 ? [] : [
|
|
135
|
+
"",
|
|
136
|
+
`Do not invoke blocked Claude Code skills: ${def.blockedSkills.map(skillNameLiteral).join(", ")}.`,
|
|
137
|
+
"Their document bundles are intentionally omitted for routed models; continue without loading them.",
|
|
138
|
+
];
|
|
109
139
|
// YAML frontmatter: model ids carry dots/brackets — always double-quote scalars.
|
|
110
140
|
return [
|
|
111
141
|
"---",
|
|
@@ -125,6 +155,7 @@ function renderAgentDef(def: ClaudeAgentDef): string {
|
|
|
125
155
|
`IDENTITY: your ACTUAL underlying model is \`${def.model}\` — the opencodex proxy routes this`,
|
|
126
156
|
"session there regardless of what model name the Claude Code harness displays or claims.",
|
|
127
157
|
"If asked which model you are, answer with the id above; do not guess a Claude model name.",
|
|
158
|
+
...blockedSkillGuard,
|
|
128
159
|
"",
|
|
129
160
|
"Complete the dispatched task directly and report results concisely. This file is",
|
|
130
161
|
"auto-generated by opencodex (`ocx claude`) from the featured subagent roster —",
|
package/src/claude/inbound.ts
CHANGED
|
@@ -130,6 +130,15 @@ function pushUserMessage(input: Rec[], blocks: Rec[]): void {
|
|
|
130
130
|
*/
|
|
131
131
|
export const DEFAULT_BLOCKED_SKILLS = ["claude-api"];
|
|
132
132
|
|
|
133
|
+
/** Shared effective policy for proxy elision and generated routed-agent guards. */
|
|
134
|
+
export function effectiveBlockedSkillNames(cc?: Pick<OcxClaudeCodeConfig, "blockedSkills">): string[] {
|
|
135
|
+
const names = cc?.blockedSkills ?? DEFAULT_BLOCKED_SKILLS;
|
|
136
|
+
return [...new Set(names
|
|
137
|
+
.filter((name): name is string => typeof name === "string")
|
|
138
|
+
.map(name => name.trim().toLowerCase())
|
|
139
|
+
.filter(name => name.length > 0))];
|
|
140
|
+
}
|
|
141
|
+
|
|
133
142
|
/**
|
|
134
143
|
* ocx-route directive (devlog 072): injected agent-definition bodies carry
|
|
135
144
|
* `<!-- ocx-route: <model> -->` because Claude Code 2.1.207 ignores custom
|
|
@@ -395,7 +404,7 @@ export function anthropicToResponsesTranslation(raw: unknown, cc?: OcxClaudeCode
|
|
|
395
404
|
const systemParts: string[] = [];
|
|
396
405
|
const topLevelSystem = systemToInstructions(raw.system);
|
|
397
406
|
if (topLevelSystem !== undefined) systemParts.push(topLevelSystem);
|
|
398
|
-
const blockedNames = (cc
|
|
407
|
+
const blockedNames = effectiveBlockedSkillNames(cc);
|
|
399
408
|
const elide: SkillElisionContext = {
|
|
400
409
|
callIds: blockedSkillCallIds(raw.messages, blockedNames),
|
|
401
410
|
names: blockedNames,
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
responsesJsonToAnthropicMessage,
|
|
20
20
|
responsesSseToAnthropicSse,
|
|
21
21
|
} from "../claude/outbound";
|
|
22
|
+
import { clearableDeadline } from "../lib/abort";
|
|
22
23
|
import { estimateTokens } from "../lib/token-estimate";
|
|
23
24
|
import { routeModel } from "../router";
|
|
24
25
|
import type { OcxConfig } from "../types";
|
|
@@ -204,24 +205,22 @@ async function anthropicNativePassthrough(
|
|
|
204
205
|
});
|
|
205
206
|
headers.set("content-type", "application/json");
|
|
206
207
|
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
finalize(504, { closeReason: "non_stream" });
|
|
220
|
-
return anthropicErrorResponse(504, "anthropic passthrough timed out waiting for response headers", "timeout_error");
|
|
221
|
-
}
|
|
208
|
+
const result = await fetchWithHeaderDeadline(
|
|
209
|
+
`${base}${pathname}${search}`,
|
|
210
|
+
{ method: "POST", headers, body: JSON.stringify(body) },
|
|
211
|
+
config.connectTimeoutMs ?? 120_000,
|
|
212
|
+
req.signal,
|
|
213
|
+
);
|
|
214
|
+
if (result.kind === "timeout") {
|
|
215
|
+
finalize(504, { closeReason: "non_stream" });
|
|
216
|
+
return anthropicErrorResponse(504, "anthropic passthrough timed out waiting for response headers", "timeout_error");
|
|
217
|
+
}
|
|
218
|
+
if (result.kind === "error") {
|
|
219
|
+
const err = result.error;
|
|
222
220
|
finalize(502, { closeReason: "non_stream" });
|
|
223
221
|
return anthropicErrorResponse(502, `anthropic passthrough failed: ${err instanceof Error ? err.message : String(err)}`, "api_error");
|
|
224
222
|
}
|
|
223
|
+
const upstream = result.upstream;
|
|
225
224
|
|
|
226
225
|
const contentType = upstream.headers.get("content-type") ?? "application/json";
|
|
227
226
|
if (upstream.ok && contentType.includes("text/event-stream") && upstream.body) {
|
|
@@ -250,6 +249,43 @@ async function anthropicNativePassthrough(
|
|
|
250
249
|
});
|
|
251
250
|
}
|
|
252
251
|
|
|
252
|
+
/**
|
|
253
|
+
* Header-phase fetch guarded by a clearable deadline (PR #136 follow-up hardening).
|
|
254
|
+
*
|
|
255
|
+
* The deadline covers ONLY the wait for response headers; once `fetch` settles —
|
|
256
|
+
* fulfilled OR rejected — the timer must die. The `finally` block guarantees
|
|
257
|
+
* `clear()` on every path (success, upstream reject, deadline expiry), fixing the
|
|
258
|
+
* timer leak where a rejected fetch left the deadline running until expiry.
|
|
259
|
+
* `didExpire()` stays truthful after `clear()` (see src/lib/abort.ts), so timeout
|
|
260
|
+
* classification inside the catch is unaffected by the finally cleanup.
|
|
261
|
+
*
|
|
262
|
+
* `makeDeadline`/`fetchImpl` are injectable for deterministic unit tests.
|
|
263
|
+
*/
|
|
264
|
+
export type HeaderDeadlineFetchResult =
|
|
265
|
+
| { kind: "response"; upstream: Response }
|
|
266
|
+
| { kind: "timeout" }
|
|
267
|
+
| { kind: "error"; error: unknown };
|
|
268
|
+
|
|
269
|
+
export async function fetchWithHeaderDeadline(
|
|
270
|
+
input: string | URL,
|
|
271
|
+
init: RequestInit,
|
|
272
|
+
timeoutMs: number,
|
|
273
|
+
parent?: AbortSignal,
|
|
274
|
+
makeDeadline: typeof clearableDeadline = clearableDeadline,
|
|
275
|
+
fetchImpl: typeof fetch = fetch,
|
|
276
|
+
): Promise<HeaderDeadlineFetchResult> {
|
|
277
|
+
const deadline = makeDeadline(timeoutMs, parent);
|
|
278
|
+
try {
|
|
279
|
+
const upstream = await fetchImpl(input, { ...init, signal: deadline.signal });
|
|
280
|
+
return { kind: "response", upstream };
|
|
281
|
+
} catch (error) {
|
|
282
|
+
if (deadline.didExpire()) return { kind: "timeout" };
|
|
283
|
+
return { kind: "error", error };
|
|
284
|
+
} finally {
|
|
285
|
+
deadline.clear();
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
253
289
|
export async function handleClaudeMessages(
|
|
254
290
|
req: Request,
|
|
255
291
|
config: OcxConfig,
|
|
@@ -27,7 +27,7 @@ import { fetchProviderQuotaReports } from "../providers/quota";
|
|
|
27
27
|
import { DEFAULT_PROVIDER_CONTEXT_CAP, globalContextCapValue, providerContextCap, providerContextCaps, setAllProviderContextCaps, setGlobalContextCapValue, setProviderContextCap } from "../providers/context-cap";
|
|
28
28
|
import { readUsageEntries } from "../usage/log";
|
|
29
29
|
import { getUsageDebugLogEntries } from "../usage/debug";
|
|
30
|
-
import { parseRange, summarizeUsage } from "../usage/summary";
|
|
30
|
+
import { parseRange, parseUsageSurface, summarizeUsage } from "../usage/summary";
|
|
31
31
|
import { stripCodexRuntimeProviderFields } from "../codex/auth-context";
|
|
32
32
|
import { getProviderRegistryEntry } from "../providers/registry";
|
|
33
33
|
import { getDebugLogEntries } from "../lib/debug-log-buffer";
|
|
@@ -358,12 +358,14 @@ export async function handleManagementAPI(req: Request, url: URL, config: OcxCon
|
|
|
358
358
|
|
|
359
359
|
if (url.pathname === "/api/usage" && req.method === "GET") {
|
|
360
360
|
const range = parseRange(url.searchParams.get("range"));
|
|
361
|
+
const surface = parseUsageSurface(url.searchParams.get("surface"));
|
|
361
362
|
const now = Date.now();
|
|
362
363
|
try {
|
|
363
|
-
return jsonResponse(summarizeUsage(readUsageEntries(), range, now));
|
|
364
|
+
return jsonResponse(summarizeUsage(readUsageEntries(), range, now, surface));
|
|
364
365
|
} catch {
|
|
365
366
|
return jsonResponse({
|
|
366
367
|
range,
|
|
368
|
+
surface,
|
|
367
369
|
since: null,
|
|
368
370
|
generatedAt: now,
|
|
369
371
|
summary: {
|
|
@@ -89,6 +89,7 @@ export function addRequestLog(entry: RequestLogEntry) {
|
|
|
89
89
|
timestamp: entry.timestamp,
|
|
90
90
|
provider: entry.provider,
|
|
91
91
|
model: entry.model,
|
|
92
|
+
...(entry.surface === "claude" ? { surface: entry.surface } : {}),
|
|
92
93
|
...(entry.resolvedModel ? { resolvedModel: entry.resolvedModel } : {}),
|
|
93
94
|
status: entry.status,
|
|
94
95
|
durationMs: entry.durationMs,
|
package/src/usage/log.ts
CHANGED
|
@@ -11,6 +11,7 @@ export interface PersistedUsageEntry {
|
|
|
11
11
|
timestamp: number;
|
|
12
12
|
provider: string;
|
|
13
13
|
model: string;
|
|
14
|
+
surface?: "claude";
|
|
14
15
|
resolvedModel?: string;
|
|
15
16
|
status: number;
|
|
16
17
|
durationMs: number;
|
|
@@ -68,6 +69,7 @@ function normalizeUsageEntry(entry: PersistedUsageEntry): PersistedUsageEntry {
|
|
|
68
69
|
timestamp: entry.timestamp,
|
|
69
70
|
provider: entry.provider,
|
|
70
71
|
model: entry.model,
|
|
72
|
+
...(entry.surface === "claude" ? { surface: entry.surface } : {}),
|
|
71
73
|
...(entry.resolvedModel ? { resolvedModel: entry.resolvedModel } : {}),
|
|
72
74
|
status: entry.status,
|
|
73
75
|
durationMs: entry.durationMs,
|
package/src/usage/summary.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { usageDisplayTotalTokens } from "./totals";
|
|
|
3
3
|
import type { PersistedUsageEntry, UsageStatus } from "./log";
|
|
4
4
|
|
|
5
5
|
export type UsageRange = "7d" | "30d" | "all";
|
|
6
|
+
export type UsageSurface = "all" | "codex" | "claude";
|
|
6
7
|
|
|
7
8
|
export interface UsageSummaryTotals {
|
|
8
9
|
requests: number;
|
|
@@ -63,6 +64,7 @@ export interface UsageProvider {
|
|
|
63
64
|
|
|
64
65
|
export interface UsageSummary {
|
|
65
66
|
range: UsageRange;
|
|
67
|
+
surface: UsageSurface;
|
|
66
68
|
since: number | null;
|
|
67
69
|
generatedAt: number;
|
|
68
70
|
summary: UsageSummaryTotals;
|
|
@@ -78,6 +80,11 @@ export function parseRange(input: string | null | undefined): UsageRange {
|
|
|
78
80
|
return "30d";
|
|
79
81
|
}
|
|
80
82
|
|
|
83
|
+
export function parseUsageSurface(input: string | null | undefined): UsageSurface {
|
|
84
|
+
if (input === "codex" || input === "claude") return input;
|
|
85
|
+
return "all";
|
|
86
|
+
}
|
|
87
|
+
|
|
81
88
|
function rangeWindow(range: UsageRange, now: number): { since: number | null; days: number } {
|
|
82
89
|
if (range === "7d") return { since: now - 7 * DAY_MS, days: 7 };
|
|
83
90
|
if (range === "30d") return { since: now - 30 * DAY_MS, days: 30 };
|
|
@@ -270,22 +277,33 @@ function buildProviders(entries: PersistedUsageEntry[], totalTokens: number): Us
|
|
|
270
277
|
return providers.sort((a, b) => b.requests - a.requests);
|
|
271
278
|
}
|
|
272
279
|
|
|
273
|
-
export function summarizeUsage(
|
|
280
|
+
export function summarizeUsage(
|
|
281
|
+
entries: PersistedUsageEntry[],
|
|
282
|
+
range: UsageRange,
|
|
283
|
+
now: number,
|
|
284
|
+
surface: UsageSurface = "all",
|
|
285
|
+
): UsageSummary {
|
|
274
286
|
const { since } = rangeWindow(range, now);
|
|
275
|
-
const
|
|
287
|
+
const filteredEntries = entries.filter(entry => {
|
|
288
|
+
if (since !== null && entry.timestamp < since) return false;
|
|
289
|
+
if (surface === "claude") return entry.surface === "claude";
|
|
290
|
+
if (surface === "codex") return entry.surface !== "claude";
|
|
291
|
+
return true;
|
|
292
|
+
});
|
|
276
293
|
const totals = blankTotals();
|
|
277
|
-
for (const entry of
|
|
294
|
+
for (const entry of filteredEntries) {
|
|
278
295
|
bumpStatus(totals, entry.usageStatus);
|
|
279
296
|
addTokens(totals, entry);
|
|
280
297
|
}
|
|
281
298
|
finalizeCoverage(totals);
|
|
282
299
|
return {
|
|
283
300
|
range,
|
|
301
|
+
surface,
|
|
284
302
|
since,
|
|
285
303
|
generatedAt: now,
|
|
286
304
|
summary: totals,
|
|
287
|
-
days: buildDayGrid(range, since, now,
|
|
288
|
-
models: buildModels(
|
|
289
|
-
providers: buildProviders(
|
|
305
|
+
days: buildDayGrid(range, since, now, filteredEntries),
|
|
306
|
+
models: buildModels(filteredEntries, totals.totalTokens),
|
|
307
|
+
providers: buildProviders(filteredEntries, totals.totalTokens),
|
|
290
308
|
};
|
|
291
309
|
}
|