@bitkyc08/opencodex 2.7.31 → 2.7.33
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.ja.md +438 -0
- package/README.ko.md +1 -1
- package/README.md +1 -1
- package/README.ru.md +2 -1
- package/README.zh-CN.md +1 -1
- package/bin/ocx.mjs +18 -1
- package/gui/dist/assets/index-D6Fcl4yM.css +1 -0
- package/gui/dist/assets/index-d63HMU0x.js +52 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic.ts +17 -2
- package/src/adapters/google-tool-schema.ts +4 -0
- package/src/adapters/google.ts +10 -2
- package/src/adapters/openai-chat.ts +36 -1
- package/src/adapters/openai-responses.ts +2 -1
- package/src/bridge.ts +12 -4
- package/src/cli/account-api.ts +4 -2
- package/src/cli/account-extended.ts +34 -0
- package/src/cli/account.ts +3 -1
- package/src/cli/claude.ts +6 -1
- package/src/cli/help.ts +25 -4
- package/src/cli/init.ts +38 -3
- package/src/cli/models.ts +206 -7
- package/src/codex/auth-api.ts +27 -3
- package/src/codex/catalog.ts +72 -2
- package/src/config.ts +60 -3
- package/src/oauth/github-copilot.ts +1 -0
- package/src/oauth/index.ts +5 -4
- package/src/oauth/kiro.ts +12 -1
- package/src/oauth/store.ts +11 -0
- package/src/oauth/types.ts +3 -1
- package/src/providers/antigravity-models.ts +103 -5
- package/src/providers/api-keys.ts +12 -0
- package/src/providers/openrouter-routing.ts +102 -0
- package/src/providers/registry.ts +7 -5
- package/src/router.ts +2 -1
- package/src/server/auth-cors.ts +5 -0
- package/src/server/management-api.ts +143 -6
- package/src/server/responses.ts +16 -4
- package/src/types.ts +42 -1
- package/src/update/index.ts +19 -2
- package/src/update/job.ts +13 -3
- package/src/usage/expected-prices.ts +10 -0
- package/src/usage/summary.ts +43 -0
- package/gui/dist/assets/index-BPa0R6EN.js +0 -46
- package/gui/dist/assets/index-BY7KvJRB.css +0 -1
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-d63HMU0x.js"></script>
|
|
20
|
+
<link rel="stylesheet" crossorigin href="/assets/index-D6Fcl4yM.css">
|
|
21
21
|
</head>
|
|
22
22
|
<body>
|
|
23
23
|
<div id="root"></div>
|
package/package.json
CHANGED
|
@@ -629,9 +629,16 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
|
|
|
629
629
|
if (typeof parsed.options.reasoning === "string" && parsed.options.reasoning !== "none") {
|
|
630
630
|
if (usesAdaptiveThinking(parsed.modelId)) {
|
|
631
631
|
// Adaptive-thinking models replace the token budget with an effort knob and reject
|
|
632
|
-
// `thinking.type: "enabled"` outright
|
|
632
|
+
// `thinking.type: "enabled"` outright. `max_tokens` still caps thinking plus visible
|
|
633
|
+
// output, so high effort needs the same total-token headroom as budget thinking or a
|
|
634
|
+
// default 8192-token request can spend everything on thought and return empty text.
|
|
633
635
|
body.thinking = { type: "adaptive" };
|
|
634
636
|
body.output_config = { effort: adaptiveEffort(parsed.options.reasoning) };
|
|
637
|
+
const maxOut = parsed.options.maxOutputTokens ?? DEFAULT_MAX_TOKENS;
|
|
638
|
+
body.max_tokens = Math.min(
|
|
639
|
+
REASONING_MAX_TOKENS_CEILING,
|
|
640
|
+
Math.max(maxOut, reasoningBudget(adaptiveEffort(parsed.options.reasoning)) + OUTPUT_HEADROOM),
|
|
641
|
+
);
|
|
635
642
|
} else {
|
|
636
643
|
// Anthropic requires max_tokens > thinking.budget_tokens (max_tokens caps thinking +
|
|
637
644
|
// visible output) and budget_tokens >= 1024. Codex sends the SAME value for both, which
|
|
@@ -713,12 +720,17 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
|
|
|
713
720
|
let currentToolCallId = "";
|
|
714
721
|
let currentToolCallName = "";
|
|
715
722
|
let pendingUsage: Record<string, number> | undefined;
|
|
723
|
+
let pendingStopReason: string | undefined;
|
|
716
724
|
let emittedDone = false;
|
|
717
725
|
|
|
718
726
|
const emitDone = function* (): Generator<AdapterEvent> {
|
|
719
727
|
if (emittedDone) return;
|
|
720
728
|
emittedDone = true;
|
|
721
|
-
yield {
|
|
729
|
+
yield {
|
|
730
|
+
type: "done",
|
|
731
|
+
usage: usageFromAnthropic(pendingUsage),
|
|
732
|
+
...(pendingStopReason ? { stopReason: pendingStopReason } : {}),
|
|
733
|
+
};
|
|
722
734
|
};
|
|
723
735
|
|
|
724
736
|
try {
|
|
@@ -796,6 +808,8 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
|
|
|
796
808
|
case "message_delta": {
|
|
797
809
|
const usage = data.usage as Record<string, number> | undefined;
|
|
798
810
|
pendingUsage = mergeAnthropicUsage(pendingUsage, usage);
|
|
811
|
+
const delta = data.delta as { stop_reason?: unknown } | undefined;
|
|
812
|
+
if (typeof delta?.stop_reason === "string") pendingStopReason = delta.stop_reason;
|
|
799
813
|
break;
|
|
800
814
|
}
|
|
801
815
|
case "message_stop": {
|
|
@@ -843,6 +857,7 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
|
|
|
843
857
|
events.push({
|
|
844
858
|
type: "done",
|
|
845
859
|
usage: usageFromAnthropic(usage),
|
|
860
|
+
...(typeof json.stop_reason === "string" ? { stopReason: json.stop_reason } : {}),
|
|
846
861
|
});
|
|
847
862
|
return events;
|
|
848
863
|
},
|
|
@@ -73,6 +73,10 @@ function sanitize(node: unknown, defs: Map<string, unknown>, depth: number): unk
|
|
|
73
73
|
if (key === "const") { out.enum = [value]; continue; }
|
|
74
74
|
if (key === "exclusiveMinimum" && typeof value === "number") { out.minimum = value; continue; }
|
|
75
75
|
if (key === "exclusiveMaximum" && typeof value === "number") { out.maximum = value; continue; }
|
|
76
|
+
if (key === "required" && Array.isArray(value)) {
|
|
77
|
+
out.required = [...new Set(value.filter((item): item is string => typeof item === "string"))];
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
76
80
|
if (key === "additionalProperties") {
|
|
77
81
|
// A boolean additionalProperties is accepted, but a nested schema is only meaningful with
|
|
78
82
|
// its own sanitize pass.
|
package/src/adapters/google.ts
CHANGED
|
@@ -21,7 +21,7 @@ import { ANTIGRAVITY_REQUEST_UA, antigravitySessionId, isLikelyRealThoughtSignat
|
|
|
21
21
|
import { sanitizeGeminiToolParameters } from "./google-tool-schema";
|
|
22
22
|
import { neutralizeIdentity } from "./identity";
|
|
23
23
|
import { antigravityUsesReplayCache, applyAntigravityReplay, clearAntigravityReplay, observeAntigravityReplay } from "./google-antigravity-replay";
|
|
24
|
-
import {
|
|
24
|
+
import { resolveAntigravityEffortWireModel } from "../providers/antigravity-models";
|
|
25
25
|
import { buildNonOpenAIToolCatalogNudgeForTools } from "./tool-catalog-nudge";
|
|
26
26
|
import { mapReasoningEffort } from "../reasoning-effort";
|
|
27
27
|
|
|
@@ -249,9 +249,17 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
|
|
|
249
249
|
const project = provider.project;
|
|
250
250
|
if (!project) throw new Error("Antigravity requires a discovered Cloud Code Assist project id (re-run `ocx login google-antigravity`).");
|
|
251
251
|
const sessionId = antigravitySessionId(parsed);
|
|
252
|
-
const
|
|
252
|
+
const mappedEffort = mapReasoningEffort(provider, parsed.modelId, parsed.options.reasoning);
|
|
253
|
+
const { wireModelId, thinkingLevel } = resolveAntigravityEffortWireModel(parsed.modelId, mappedEffort);
|
|
253
254
|
antigravityModel = wireModelId;
|
|
254
255
|
antigravitySession = sessionId;
|
|
256
|
+
// Effort → thinkingConfig for CCA (CLIProxyAPI proven: request.generationConfig.thinkingConfig).
|
|
257
|
+
// Suffix/compat IDs return thinkingLevel=undefined — the suffix IS the effort, no contradiction.
|
|
258
|
+
if (thinkingLevel) {
|
|
259
|
+
const gc = (body.generationConfig ?? {}) as Record<string, unknown>;
|
|
260
|
+
gc.thinkingConfig = { thinkingLevel };
|
|
261
|
+
body.generationConfig = gc;
|
|
262
|
+
}
|
|
255
263
|
// Reasoning continuity: Gemini models re-inject cached thoughtSignatures; Claude-on-Antigravity
|
|
256
264
|
// sanitizes signatures inline (no cache). Both guard against the upstream 400 on bad signatures.
|
|
257
265
|
if (Array.isArray((body as { contents?: unknown[] }).contents)) {
|
|
@@ -6,6 +6,7 @@ import { redactSecretString } from "../lib/redact";
|
|
|
6
6
|
import { contentPartsToText } from "./image";
|
|
7
7
|
import { neutralizeIdentity } from "./identity";
|
|
8
8
|
import { buildNonOpenAIToolCatalogNudgeForTools, shouldInjectNonOpenAIToolCatalogNudge } from "./tool-catalog-nudge";
|
|
9
|
+
import { openRouterProviderPayload, resolveOpenRouterRouting } from "../providers/openrouter-routing";
|
|
9
10
|
|
|
10
11
|
// Providers may opt into stripping one trailing "[...]" group from the wire model id.
|
|
11
12
|
// Z.AI needs this because its OpenAI path rejects glm-5.2[1m] with 400 code 1211;
|
|
@@ -337,6 +338,28 @@ function isXaiSchemaTarget(provider: OcxProviderConfig): boolean {
|
|
|
337
338
|
}
|
|
338
339
|
}
|
|
339
340
|
|
|
341
|
+
function isKimiSchemaTarget(provider: OcxProviderConfig): boolean {
|
|
342
|
+
try {
|
|
343
|
+
return new URL(provider.baseUrl).hostname === "api.kimi.com";
|
|
344
|
+
} catch {
|
|
345
|
+
return false;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Kimi requires function.parameters.type to be exactly "object" at the root.
|
|
351
|
+
* Codex tools with oneOf/anyOf schemas omit the root type, causing 400 errors.
|
|
352
|
+
* Add type: "object" at the root while preserving oneOf, $defs, and other schema keys.
|
|
353
|
+
*/
|
|
354
|
+
function ensureKimiRootObjectType(parameters: unknown): Record<string, unknown> {
|
|
355
|
+
if (!parameters || typeof parameters !== "object" || Array.isArray(parameters)) {
|
|
356
|
+
return { type: "object", properties: {} };
|
|
357
|
+
}
|
|
358
|
+
const obj = parameters as Record<string, unknown>;
|
|
359
|
+
if (obj.type === "object") return obj;
|
|
360
|
+
return { ...obj, type: "object" };
|
|
361
|
+
}
|
|
362
|
+
|
|
340
363
|
function expandXaiRootObjectSchemas(schema: unknown): Record<string, unknown>[] | undefined {
|
|
341
364
|
if (!schema || typeof schema !== "object" || Array.isArray(schema)) return undefined;
|
|
342
365
|
const obj = schema as Record<string, unknown>;
|
|
@@ -379,8 +402,13 @@ function toolsToChatFormat(parsed: OcxParsedRequest, provider: OcxProviderConfig
|
|
|
379
402
|
: parsed.context.tools;
|
|
380
403
|
if (tools.length === 0) return undefined;
|
|
381
404
|
const xaiTarget = isXaiSchemaTarget(provider);
|
|
405
|
+
const kimiTarget = isKimiSchemaTarget(provider);
|
|
382
406
|
const formatted = tools.flatMap(t => {
|
|
383
|
-
const parameters = xaiTarget
|
|
407
|
+
const parameters = xaiTarget
|
|
408
|
+
? normalizeXaiToolParameters(t.parameters)
|
|
409
|
+
: kimiTarget
|
|
410
|
+
? ensureKimiRootObjectType(t.parameters)
|
|
411
|
+
: t.parameters;
|
|
384
412
|
if (parameters === undefined) return [];
|
|
385
413
|
return [{
|
|
386
414
|
type: "function",
|
|
@@ -467,6 +495,8 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
467
495
|
messages,
|
|
468
496
|
stream: parsed.stream,
|
|
469
497
|
};
|
|
498
|
+
const openRouterRouting = resolveOpenRouterRouting(provider, parsed.modelId);
|
|
499
|
+
if (openRouterRouting) body.provider = openRouterProviderPayload(openRouterRouting);
|
|
470
500
|
if (tools) body.tools = tools;
|
|
471
501
|
if (tools && toolChoice !== undefined) {
|
|
472
502
|
body.tool_choice = modelInList(provider.autoToolChoiceOnlyModels, parsed.modelId)
|
|
@@ -502,6 +532,11 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
502
532
|
if (parsed.options.frequencyPenalty !== undefined && !modelInList(provider.noPenaltyModels, parsed.modelId)) {
|
|
503
533
|
body.frequency_penalty = parsed.options.frequencyPenalty;
|
|
504
534
|
}
|
|
535
|
+
// prompt_cache_key is an OpenAI-specific chat extension; strict backends (Groq,
|
|
536
|
+
// Cerebras, etc.) reject unknown fields. Only forward when the provider opts in.
|
|
537
|
+
if (provider.promptCacheKey && parsed.options.promptCacheKey !== undefined) {
|
|
538
|
+
body.prompt_cache_key = parsed.options.promptCacheKey;
|
|
539
|
+
}
|
|
505
540
|
|
|
506
541
|
if (tools) {
|
|
507
542
|
// Default-ON for chat-completions providers (user decision 260709): the buffered
|
|
@@ -25,7 +25,7 @@ export const FORWARD_HEADERS = [
|
|
|
25
25
|
"x-responsesapi-include-timing-metrics",
|
|
26
26
|
];
|
|
27
27
|
|
|
28
|
-
function sanitizeReasoningInputContent(body: unknown): unknown {
|
|
28
|
+
export function sanitizeReasoningInputContent(body: unknown): unknown {
|
|
29
29
|
if (!body || typeof body !== "object" || Array.isArray(body)) return body;
|
|
30
30
|
const raw = body as Record<string, unknown>;
|
|
31
31
|
if (!Array.isArray(raw.input)) return body;
|
|
@@ -57,6 +57,7 @@ function stripInvalidItemIds(body: unknown): unknown {
|
|
|
57
57
|
|
|
58
58
|
const validPrefixes: Record<string, string> = {
|
|
59
59
|
message: "msg_",
|
|
60
|
+
agent_message: "amsg_",
|
|
60
61
|
reasoning: "rs_",
|
|
61
62
|
function_call: "fc_",
|
|
62
63
|
custom_tool_call: "ctc_",
|
package/src/bridge.ts
CHANGED
|
@@ -634,12 +634,17 @@ export function bridgeToResponsesSSE(
|
|
|
634
634
|
finishedItems.push(item as OutputItem);
|
|
635
635
|
outputIndex++;
|
|
636
636
|
}
|
|
637
|
-
const
|
|
637
|
+
const truncated = event.stopReason === "max_tokens";
|
|
638
|
+
const response = {
|
|
639
|
+
...responseSnapshot(truncated ? "incomplete" : "completed", finishedItems),
|
|
640
|
+
usage: responsesUsage(event.usage),
|
|
641
|
+
...(truncated ? { incomplete_details: { reason: "max_output_tokens" } } : {}),
|
|
642
|
+
};
|
|
638
643
|
options?.onCompletedResponse?.(response);
|
|
639
|
-
emit("response.completed", {
|
|
644
|
+
emit(truncated ? "response.incomplete" : "response.completed", {
|
|
640
645
|
response,
|
|
641
646
|
});
|
|
642
|
-
reportTerminal("completed");
|
|
647
|
+
reportTerminal(truncated ? "incomplete" : "completed");
|
|
643
648
|
terminated = true;
|
|
644
649
|
break;
|
|
645
650
|
}
|
|
@@ -737,6 +742,7 @@ export function buildResponseJSON(
|
|
|
737
742
|
const output: OutputItem[] = [];
|
|
738
743
|
let usage: OcxUsage | undefined;
|
|
739
744
|
let errorMessage: string | undefined;
|
|
745
|
+
let stopReason: string | undefined;
|
|
740
746
|
let compactionText = "";
|
|
741
747
|
|
|
742
748
|
let currentText = "";
|
|
@@ -911,6 +917,7 @@ export function buildResponseJSON(
|
|
|
911
917
|
break;
|
|
912
918
|
case "done":
|
|
913
919
|
usage = e.usage;
|
|
920
|
+
stopReason = e.stopReason;
|
|
914
921
|
break;
|
|
915
922
|
}
|
|
916
923
|
}
|
|
@@ -925,9 +932,10 @@ export function buildResponseJSON(
|
|
|
925
932
|
return {
|
|
926
933
|
id: responseId, object: "response",
|
|
927
934
|
created_at: Math.floor(Date.now() / 1000),
|
|
928
|
-
status: errorMessage ? "failed" : "completed",
|
|
935
|
+
status: errorMessage ? "failed" : stopReason === "max_tokens" ? "incomplete" : "completed",
|
|
929
936
|
model: modelId, output,
|
|
930
937
|
...(errorMessage ? { error: { message: errorMessage } } : {}),
|
|
938
|
+
...(!errorMessage && stopReason === "max_tokens" ? { incomplete_details: { reason: "max_output_tokens" } } : {}),
|
|
931
939
|
usage: responsesUsage(usage),
|
|
932
940
|
};
|
|
933
941
|
}
|
package/src/cli/account-api.ts
CHANGED
|
@@ -143,6 +143,7 @@ export interface ProviderQuotaReportDto {
|
|
|
143
143
|
|
|
144
144
|
interface CodexAccountDto {
|
|
145
145
|
id: string;
|
|
146
|
+
alias?: string;
|
|
146
147
|
email?: string;
|
|
147
148
|
plan?: string;
|
|
148
149
|
isMain?: boolean;
|
|
@@ -189,7 +190,7 @@ export async function fetchCodexRows(
|
|
|
189
190
|
provider: "openai",
|
|
190
191
|
type: "codex" as const,
|
|
191
192
|
id: a.id,
|
|
192
|
-
label: a.plan ?? a.email,
|
|
193
|
+
label: a.alias ?? a.plan ?? a.email,
|
|
193
194
|
email: a.email,
|
|
194
195
|
plan: a.plan,
|
|
195
196
|
active: a.id === activeId,
|
|
@@ -201,6 +202,7 @@ export async function fetchCodexRows(
|
|
|
201
202
|
|
|
202
203
|
interface OAuthAccountDto {
|
|
203
204
|
id: string;
|
|
205
|
+
alias?: string;
|
|
204
206
|
email?: string;
|
|
205
207
|
active?: boolean;
|
|
206
208
|
needsReauth?: boolean;
|
|
@@ -216,7 +218,7 @@ async function fetchOAuthRows(deps: AccountDeps, baseUrl: string, name: string):
|
|
|
216
218
|
provider: name,
|
|
217
219
|
type: "oauth" as const,
|
|
218
220
|
id: a.id,
|
|
219
|
-
label: a.email ?? `Account ${i + 1}`,
|
|
221
|
+
label: a.alias ?? a.email ?? `Account ${i + 1}`,
|
|
220
222
|
email: a.email,
|
|
221
223
|
active: a.active ?? a.id === activeId,
|
|
222
224
|
needsReauth: a.needsReauth,
|
|
@@ -17,6 +17,7 @@ const AUTO_NOTE = "auto (no pin — lowest-usage account is selected per request
|
|
|
17
17
|
const EXTENDED_USAGE = `Usage:
|
|
18
18
|
ocx account refresh <provider> [--json]
|
|
19
19
|
ocx account auto-switch <provider> <on|off|status|threshold <0-100>> [--json]
|
|
20
|
+
ocx account alias <provider> <id|main> <display-name|-> [--json]
|
|
20
21
|
ocx account remove <provider> <id|main> --yes [--json]
|
|
21
22
|
ocx account add-key <provider> [--label <label>] [--json]`;
|
|
22
23
|
const PIPE_GUIDANCE = `Pipe the API key on stdin, for example:
|
|
@@ -280,3 +281,36 @@ export async function cmdAddKey(args: string[], deps: AccountDeps): Promise<numb
|
|
|
280
281
|
console.log(output.replaceAll(key, "[redacted]"));
|
|
281
282
|
return 0;
|
|
282
283
|
}
|
|
284
|
+
|
|
285
|
+
export async function cmdAlias(args: string[], deps: AccountDeps): Promise<number> {
|
|
286
|
+
const wantsJson = flag(args, "--json");
|
|
287
|
+
const name = args.shift();
|
|
288
|
+
const requestedId = args.shift();
|
|
289
|
+
const requestedAlias = args.shift();
|
|
290
|
+
if (!name || !requestedId || requestedAlias === undefined || args.length) return usage();
|
|
291
|
+
const classified = configAndType(deps, name);
|
|
292
|
+
if ("error" in classified) return usage(`Error: ${classified.error}`);
|
|
293
|
+
const id = classified.type === "codex" && requestedId === "main" ? MAIN_ID : requestedId;
|
|
294
|
+
if (id === MAIN_ID) return usage("Error: the main Codex App login cannot be renamed");
|
|
295
|
+
const alias = requestedAlias === "-" ? "" : requestedAlias.trim();
|
|
296
|
+
if (alias.length > 80 || /[\x00-\x1f\x7f]/.test(alias)) return usage("Error: alias must be at most 80 printable characters");
|
|
297
|
+
const baseUrl = await resolveBaseUrl(deps);
|
|
298
|
+
if (!baseUrl) return proxyUnreachable();
|
|
299
|
+
const path = classified.type === "codex"
|
|
300
|
+
? "/api/codex-auth/accounts/alias"
|
|
301
|
+
: classified.type === "oauth"
|
|
302
|
+
? "/api/oauth/accounts/alias"
|
|
303
|
+
: "/api/providers/keys/alias";
|
|
304
|
+
const body = classified.type === "codex"
|
|
305
|
+
? { id, alias }
|
|
306
|
+
: classified.type === "oauth"
|
|
307
|
+
? { provider: name, accountId: id, alias }
|
|
308
|
+
: { name, id, alias };
|
|
309
|
+
const response = await apiJson(deps, baseUrl, "PUT", path, body);
|
|
310
|
+
if (response.status === 0) return proxyUnreachable();
|
|
311
|
+
if (response.status !== 200) return apiError(response.json, `failed to rename ${requestedId}`);
|
|
312
|
+
const result = { ok: true, provider: name, id, alias: alias || null };
|
|
313
|
+
if (wantsJson) console.log(JSON.stringify(result, null, 2));
|
|
314
|
+
else console.log(alias ? `${name}: ${requestedId} is now “${alias}”` : `${name}: cleared alias for ${requestedId}`);
|
|
315
|
+
return 0;
|
|
316
|
+
}
|
package/src/cli/account.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { loadConfig } from "../config";
|
|
3
3
|
import { providerCodexAccountMode } from "../providers/registry";
|
|
4
4
|
import type { OcxConfig } from "../types";
|
|
5
|
-
import { cmdAddKey, cmdAutoSwitch, cmdRefresh, cmdRemove } from "./account-extended";
|
|
5
|
+
import { cmdAddKey, cmdAlias, cmdAutoSwitch, cmdRefresh, cmdRemove } from "./account-extended";
|
|
6
6
|
import { apiError, apiJson, classifyAccount, fetchRows, proxyUnreachable, resolveBaseUrl, type AccountDeps, type AccountRow, type AccountType, type ApiResult }
|
|
7
7
|
from "./account-api";
|
|
8
8
|
|
|
@@ -21,6 +21,7 @@ const ACCOUNT_USAGE = `Usage:
|
|
|
21
21
|
ocx account use <provider> <account-or-key-id|main> [--json]
|
|
22
22
|
ocx account refresh <provider> [--json]
|
|
23
23
|
ocx account auto-switch <provider> <on|off|status|threshold <0-100>> [--json]
|
|
24
|
+
ocx account alias <provider> <account-or-key-id> <display-name|-> [--json]
|
|
24
25
|
ocx account remove <provider> <account-or-key-id|main> --yes [--json]
|
|
25
26
|
ocx account add-key <provider> [--label <label>] [--json]
|
|
26
27
|
|
|
@@ -254,6 +255,7 @@ export async function cmdAccount(args: string[], deps: AccountDeps = {}): Promis
|
|
|
254
255
|
if (sub === "use") return await cmdUse(rest, deps);
|
|
255
256
|
if (sub === "refresh") return await cmdRefresh(rest, deps);
|
|
256
257
|
if (sub === "auto-switch") return await cmdAutoSwitch(rest, deps);
|
|
258
|
+
if (sub === "alias" || sub === "rename") return await cmdAlias(rest, deps);
|
|
257
259
|
if (sub === "remove") return await cmdRemove(rest, deps);
|
|
258
260
|
if (sub === "add-key") return await cmdAddKey(rest, deps);
|
|
259
261
|
console.error(ACCOUNT_USAGE);
|
package/src/cli/claude.ts
CHANGED
|
@@ -73,7 +73,12 @@ export function buildClaudeEnv(config: OcxConfig, port: number, base: ClaudeLaun
|
|
|
73
73
|
// setDefault: an explicit user export (e.g. =0, isEnvTruthy-false) still wins.
|
|
74
74
|
// Intentional contract change: settings.env model slots are also stripped in
|
|
75
75
|
// ocx claude runs — use the top-level settings "model" field or opt out.
|
|
76
|
-
|
|
76
|
+
// Claude Code 2.1.206+ also treats this as a host-auth assertion. Injecting it
|
|
77
|
+
// without a host token makes a valid claude.ai subscription look logged out,
|
|
78
|
+
// so the guard is only safe when opencodex actually owns authentication.
|
|
79
|
+
if (env.ANTHROPIC_AUTH_TOKEN) {
|
|
80
|
+
setDefault("CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST", "1");
|
|
81
|
+
}
|
|
77
82
|
// Opt-in effort forcing (devlog 136 B6): opus-shaped aliases already carry
|
|
78
83
|
// output_config.effort, so this is OFF unless the user enables it in config.
|
|
79
84
|
if (config.claudeCode?.alwaysEnableEffort === true) {
|
package/src/cli/help.ts
CHANGED
|
@@ -94,9 +94,18 @@ const helpEntries: Record<string, HelpEntry> = {
|
|
|
94
94
|
],
|
|
95
95
|
},
|
|
96
96
|
models: {
|
|
97
|
-
usage: "ocx models [--provider <name>] [--json]",
|
|
98
|
-
summary: "List
|
|
99
|
-
details: [
|
|
97
|
+
usage: "ocx models [list] [--provider <name>] [--json] | add <provider> <modelId> [opts] | remove <id|provider/modelId> [--yes] | list-custom [--json]",
|
|
98
|
+
summary: "List models and manage custom (manually registered) models.",
|
|
99
|
+
details: [
|
|
100
|
+
"List available models from static config with no subcommand (liveModels may add more at runtime).",
|
|
101
|
+
"add: register a model the provider catalog does not advertise yet.",
|
|
102
|
+
" --display-name <name> Human label (no slashes).",
|
|
103
|
+
" --context-window <tokens> e.g. 200000.",
|
|
104
|
+
" --modalities text,image Comma-separated (text|image|audio).",
|
|
105
|
+
"remove: delete a custom model by UUID or <provider>/<modelId>.",
|
|
106
|
+
"list-custom: show all custom models.",
|
|
107
|
+
"Changes apply immediately to a running proxy (catalog sync).",
|
|
108
|
+
],
|
|
100
109
|
},
|
|
101
110
|
claude: {
|
|
102
111
|
usage: "ocx claude [claude args...]",
|
|
@@ -113,6 +122,17 @@ const helpEntries: Record<string, HelpEntry> = {
|
|
|
113
122
|
usage: "ocx restart",
|
|
114
123
|
summary: "Stop the proxy and restart it (background). Equivalent to stop + ensure.",
|
|
115
124
|
},
|
|
125
|
+
v2: {
|
|
126
|
+
usage: "ocx v2 <status|on|off|mode <v1|default|v2>|threads <n>>",
|
|
127
|
+
summary: "Toggle the Codex multi_agent_v2 feature (multi-agent surface).",
|
|
128
|
+
details: [
|
|
129
|
+
"status Show flag, multi-agent mode, and thread limit.",
|
|
130
|
+
"on | off Enable/disable multi_agent_v2 (catalog resyncs).",
|
|
131
|
+
"mode <v1|default|v2> Force all models to one surface, or respect upstream pins.",
|
|
132
|
+
"threads <n> Set max_concurrent_threads_per_session (integer >= 1).",
|
|
133
|
+
"Flips preserve the active thread limit while moving between v1/v2 modes.",
|
|
134
|
+
],
|
|
135
|
+
},
|
|
116
136
|
health: {
|
|
117
137
|
usage: "ocx health [--json]",
|
|
118
138
|
summary: "Check proxy health. Exits 0 if healthy, 1 otherwise.",
|
|
@@ -156,10 +176,11 @@ Usage:
|
|
|
156
176
|
ocx gui Open the opencodex dashboard
|
|
157
177
|
ocx update [--tag <tag>] Update opencodex (keeps preview installs on @preview)
|
|
158
178
|
ocx restart Stop and restart the proxy
|
|
179
|
+
ocx v2 <sub> multi_agent_v2 surface (status|on|off|mode|threads)
|
|
159
180
|
ocx health [--json] Check proxy health (exit 0=healthy, 1=not)
|
|
160
181
|
ocx provider <sub> Manage providers (list|add|remove|show|set-default)
|
|
161
182
|
ocx account <sub> Accounts/keys (list|current|use|refresh|auto-switch|remove|add-key)
|
|
162
|
-
ocx models
|
|
183
|
+
ocx models <sub> List models; manage custom models (add|remove|list-custom)
|
|
163
184
|
ocx claude [args...] Launch Claude Code wired to the proxy (model discovery on)
|
|
164
185
|
ocx help [command] Show help
|
|
165
186
|
ocx --version | -v Print version
|
package/src/cli/init.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as readline from "node:readline";
|
|
2
|
+
import { constants as fsConstants, copyFileSync, existsSync, readFileSync, unlinkSync } from "node:fs";
|
|
2
3
|
import { injectCodexConfig } from "../codex/inject";
|
|
3
|
-
import { getDefaultConfig, isValidProviderName, saveConfig } from "../config";
|
|
4
|
+
import { classifyOpenAiTierBackup, getConfigPath, getDefaultConfig, isValidProviderName, saveConfig } from "../config";
|
|
4
5
|
import { enrichProviderFromCatalog } from "../oauth/key-providers";
|
|
5
6
|
import { deriveInitProviders } from "../providers/derive";
|
|
6
7
|
import type { OcxConfig, OcxProviderConfig } from "../types";
|
|
@@ -42,7 +43,7 @@ const KIND_HEADING: Record<InitKind, string> = {
|
|
|
42
43
|
};
|
|
43
44
|
|
|
44
45
|
function printMenu(providers: InitProvider[]): void {
|
|
45
|
-
console.log("
|
|
46
|
+
console.log("Choose your default provider (you can add more later):");
|
|
46
47
|
let lastKind: InitKind | null = null;
|
|
47
48
|
providers.forEach((p, i) => {
|
|
48
49
|
if (p.kind !== lastKind) { console.log(`\n ${KIND_HEADING[p.kind]}:`); lastKind = p.kind; }
|
|
@@ -53,6 +54,33 @@ function printMenu(providers: InitProvider[]): void {
|
|
|
53
54
|
|
|
54
55
|
const envKeyFor = (id: string) => `${id.toUpperCase().replace(/[^A-Z0-9]+/g, "_")}_API_KEY`;
|
|
55
56
|
|
|
57
|
+
/** Post-init cleanup of `.pre-openai-tiers-v2.bak` with rollback preservation (issue #257). */
|
|
58
|
+
export function cleanupOpenAiTierBackupAfterInit(configPath = getConfigPath()): void {
|
|
59
|
+
const backup = `${configPath}.pre-openai-tiers-v2.bak`;
|
|
60
|
+
try {
|
|
61
|
+
if (!existsSync(backup)) return;
|
|
62
|
+
if (classifyOpenAiTierBackup(readFileSync(backup)) === "stale") {
|
|
63
|
+
unlinkSync(backup);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
// Publish the preserved snapshot with a no-replace copy (COPYFILE_EXCL) so a
|
|
67
|
+
// destination collision (frozen/rolled-back clock, pre-created file) can never
|
|
68
|
+
// silently overwrite another rollback snapshot; retry with a sequence suffix.
|
|
69
|
+
for (let attempt = 0; attempt < 16; attempt++) {
|
|
70
|
+
const preserved = `${configPath}.pre-openai-tiers-v1-rollback.${Date.now()}${attempt ? `-${attempt}` : ""}.bak`;
|
|
71
|
+
try {
|
|
72
|
+
copyFileSync(backup, preserved, fsConstants.COPYFILE_EXCL);
|
|
73
|
+
} catch (error) {
|
|
74
|
+
if ((error as NodeJS.ErrnoException).code === "EEXIST") continue;
|
|
75
|
+
throw error;
|
|
76
|
+
}
|
|
77
|
+
unlinkSync(backup);
|
|
78
|
+
console.warn(`⚠️ Kept your pre-migration config rollback snapshot at ${preserved}`);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
} catch { /* cleanup is best-effort; never block init on backup housekeeping */ }
|
|
82
|
+
}
|
|
83
|
+
|
|
56
84
|
export async function runInit(): Promise<void> {
|
|
57
85
|
const prompt = createPrompt();
|
|
58
86
|
console.log("\n🔧 opencodex (ocx) setup\n");
|
|
@@ -60,7 +88,7 @@ export async function runInit(): Promise<void> {
|
|
|
60
88
|
const providers = buildInitProviders();
|
|
61
89
|
printMenu(providers);
|
|
62
90
|
|
|
63
|
-
const choice = await prompt.ask("\nSelect provider (number): ");
|
|
91
|
+
const choice = await prompt.ask("\nSelect default provider (number): ");
|
|
64
92
|
const idx = parseInt(choice, 10) - 1;
|
|
65
93
|
|
|
66
94
|
let providerName: string;
|
|
@@ -136,6 +164,13 @@ export async function runInit(): Promise<void> {
|
|
|
136
164
|
};
|
|
137
165
|
|
|
138
166
|
saveConfig(config);
|
|
167
|
+
// Init writes a fresh config, so a stale pre-migration backup from a previous
|
|
168
|
+
// installation would make the next `ocx start` crash on a stale-backup
|
|
169
|
+
// collision (issue #257). But only a STALE backup (unparseable, or already a
|
|
170
|
+
// post-migration v2 snapshot) may be deleted; a backup that still parses as a
|
|
171
|
+
// valid pre-migration (v1) config is a user-intentional rollback point and is
|
|
172
|
+
// preserved by renaming it out of the collision path (sol review 260722).
|
|
173
|
+
cleanupOpenAiTierBackupAfterInit();
|
|
139
174
|
console.log(`\n✅ Config saved to ~/.opencodex/config.json`);
|
|
140
175
|
if (oauthHint) console.log(`🔐 Authenticate this provider with: ocx login ${providerName}`);
|
|
141
176
|
|