@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/src/types.ts
CHANGED
|
@@ -211,7 +211,7 @@ export type AdapterEvent =
|
|
|
211
211
|
// the SAME output index, so the activity animates instead of flashing completed instantly.
|
|
212
212
|
| { type: "web_search_call_begin"; id: string }
|
|
213
213
|
| { type: "web_search_call_end"; id: string; queries: string[]; status?: "completed" | "failed"; sources?: OcxUrlCitation[] }
|
|
214
|
-
| { type: "done"; usage?: OcxUsage }
|
|
214
|
+
| { type: "done"; usage?: OcxUsage; stopReason?: string }
|
|
215
215
|
// `usage` carries best-effort partial consumption when a turn dies before a clean done
|
|
216
216
|
// (e.g. cursor upstream 502 mid-stream), so failed requests can log real token counts.
|
|
217
217
|
| { type: "error"; message: string; usage?: OcxUsage };
|
|
@@ -347,6 +347,24 @@ export interface OcxClaudeCodeConfig {
|
|
|
347
347
|
visionSidecar?: { backend?: "openai" | "anthropic"; model?: string };
|
|
348
348
|
}
|
|
349
349
|
|
|
350
|
+
/** 사용자가 대시보드에서 직접 추가한 커스텀 모델 정의. */
|
|
351
|
+
export interface OcxCustomModel {
|
|
352
|
+
/** 고유 ID (crypto.randomUUID()) */
|
|
353
|
+
id: string;
|
|
354
|
+
/** 프로바이더 키 (기존 providers[name]) */
|
|
355
|
+
provider: string;
|
|
356
|
+
/** 모델 슬러그 (프로바이더 접두사 없는 bare id) */
|
|
357
|
+
modelId: string;
|
|
358
|
+
/** 인간 가독 표시명 (선택, 슬래시 불가) */
|
|
359
|
+
displayName?: string;
|
|
360
|
+
/** 컨텍스트 윈도우 (토큰) */
|
|
361
|
+
contextWindow?: number;
|
|
362
|
+
/** 입력 모달리티 (선택, 기본 ["text"]) */
|
|
363
|
+
inputModalities?: string[];
|
|
364
|
+
/** 추가 시각 (ISO 8601) */
|
|
365
|
+
addedAt?: string;
|
|
366
|
+
}
|
|
367
|
+
|
|
350
368
|
export interface OcxConfig {
|
|
351
369
|
port: number;
|
|
352
370
|
providers: Record<string, OcxProviderConfig>;
|
|
@@ -402,6 +420,8 @@ export interface OcxConfig {
|
|
|
402
420
|
* are omitted from the bare /v1/models list.
|
|
403
421
|
*/
|
|
404
422
|
disabledModels?: string[];
|
|
423
|
+
/** 사용자가 대시보드에서 직접 추가한 커스텀 모델 목록. */
|
|
424
|
+
customModels?: OcxCustomModel[];
|
|
405
425
|
/**
|
|
406
426
|
* Shadow call intercept: redirect Codex Desktop's hard-coded gpt-5.4-mini helper calls
|
|
407
427
|
* (title generation, commit messages, skill orchestration) to a user-chosen model.
|
|
@@ -585,6 +605,15 @@ export interface OcxWebSearchSidecarConfig {
|
|
|
585
605
|
routedModelStallTimeoutMs?: number;
|
|
586
606
|
}
|
|
587
607
|
|
|
608
|
+
export interface OpenRouterProviderRouting {
|
|
609
|
+
/** OpenRouter provider slugs to try first, in priority order. */
|
|
610
|
+
order?: string[];
|
|
611
|
+
/** Restrict routing to these OpenRouter provider slugs. */
|
|
612
|
+
only?: string[];
|
|
613
|
+
/** Whether OpenRouter may use providers outside `order`. Defaults to OpenRouter's policy. */
|
|
614
|
+
allowFallbacks?: boolean;
|
|
615
|
+
}
|
|
616
|
+
|
|
588
617
|
export interface OcxProviderConfig {
|
|
589
618
|
adapter: string;
|
|
590
619
|
baseUrl: string;
|
|
@@ -633,6 +662,10 @@ export interface OcxProviderConfig {
|
|
|
633
662
|
/** Model-specific max input token limits. Values cap auto_compact_token_limit. */
|
|
634
663
|
modelMaxInputTokens?: Record<string, number>;
|
|
635
664
|
headers?: Record<string, string>;
|
|
665
|
+
/** Default provider-routing preferences for models sent through the canonical OpenRouter API. */
|
|
666
|
+
openRouterRouting?: OpenRouterProviderRouting;
|
|
667
|
+
/** Exact model-id overrides for `openRouterRouting`. Each matching entry replaces the default. */
|
|
668
|
+
modelOpenRouterRouting?: Record<string, OpenRouterProviderRouting>;
|
|
636
669
|
/**
|
|
637
670
|
* "key" (default): authenticate upstream with `apiKey`.
|
|
638
671
|
* "forward": relay the caller's incoming auth headers verbatim (OAuth passthrough; gpt only).
|
|
@@ -692,6 +725,12 @@ export interface OcxProviderConfig {
|
|
|
692
725
|
* only on explicit `true`. See devlog/_plan/260709_parallel_tool_calls.
|
|
693
726
|
*/
|
|
694
727
|
parallelToolCalls?: boolean;
|
|
728
|
+
/**
|
|
729
|
+
* Opt-in: forward `prompt_cache_key` to the upstream `/chat/completions` body.
|
|
730
|
+
* OpenAI-specific extension; strict backends (Groq, Cerebras, etc.) reject unknown
|
|
731
|
+
* fields. Default off; only enable for providers that document this parameter.
|
|
732
|
+
*/
|
|
733
|
+
promptCacheKey?: boolean;
|
|
695
734
|
/** Model ids whose tool_choice only accepts `auto` or `none`; forced/named choices are downgraded. */
|
|
696
735
|
autoToolChoiceOnlyModels?: string[];
|
|
697
736
|
/** Model ids that expect prior assistant `reasoning_content` to be preserved in chat history. */
|
|
@@ -768,6 +807,8 @@ export const OPENAI_PROVIDER_TIER_VERSION = 2 as const;
|
|
|
768
807
|
export interface CodexAccount {
|
|
769
808
|
id: string;
|
|
770
809
|
email: string;
|
|
810
|
+
/** User-owned display label; never participates in routing or identity checks. */
|
|
811
|
+
alias?: string;
|
|
771
812
|
plan?: string;
|
|
772
813
|
chatgptAccountId?: string;
|
|
773
814
|
logLabel?: string;
|
package/src/update/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { spawnSync } from "node:child_process";
|
|
1
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
2
2
|
import { readFileSync, readdirSync } from "node:fs";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import { dirname, join } from "node:path";
|
|
@@ -260,7 +260,24 @@ export async function runUpdate(): Promise<void> {
|
|
|
260
260
|
windowsHide: true,
|
|
261
261
|
});
|
|
262
262
|
if (svcStdio === "pipe") logSpawnOutput("", svc);
|
|
263
|
-
if (svc.status !== 0)
|
|
263
|
+
if (svc.status !== 0) {
|
|
264
|
+
// On Windows, schtasks /create requires elevation. The CLI inherits the
|
|
265
|
+
// user's (non-admin) token, so the service reinstall can fail with access
|
|
266
|
+
// denied. Fall back to a direct detached proxy start so the update never
|
|
267
|
+
// leaves the user without a running proxy.
|
|
268
|
+
console.warn("⚠️ Service refresh failed — starting the proxy directly instead.");
|
|
269
|
+
console.warn(" Run 'ocx service install' as administrator to refresh the background service.");
|
|
270
|
+
const env = { ...process.env };
|
|
271
|
+
delete env.OCX_SERVICE;
|
|
272
|
+
const child = spawn(process.execPath, [process.argv[1], "start", "--port", String(capturedListen.port)], {
|
|
273
|
+
detached: true,
|
|
274
|
+
stdio: "ignore",
|
|
275
|
+
windowsHide: true,
|
|
276
|
+
env,
|
|
277
|
+
});
|
|
278
|
+
child.unref();
|
|
279
|
+
console.log(`✅ Proxy starting on port ${capturedListen.port}.`);
|
|
280
|
+
}
|
|
264
281
|
} finally {
|
|
265
282
|
if (prevBake === undefined) delete process.env.OCX_BAKE_PORT;
|
|
266
283
|
else process.env.OCX_BAKE_PORT = prevBake;
|
package/src/update/job.ts
CHANGED
|
@@ -330,17 +330,27 @@ async function restartAfterUpdate(
|
|
|
330
330
|
}
|
|
331
331
|
const prevBake = process.env.OCX_BAKE_PORT;
|
|
332
332
|
process.env.OCX_BAKE_PORT = String(Math.trunc(port));
|
|
333
|
+
let serviceOk = false;
|
|
333
334
|
try {
|
|
334
335
|
const run = io.runService ?? ((j, bin, args) => runLoggedCommand(j, bin, args, RESTART_TIMEOUT_MS));
|
|
335
336
|
const result = run(job, cmd.bin, cmd.args);
|
|
336
|
-
|
|
337
|
-
|
|
337
|
+
serviceOk = result.status === 0;
|
|
338
|
+
if (!serviceOk) {
|
|
339
|
+
// On Windows, `schtasks /create` requires an elevated token. The update worker
|
|
340
|
+
// inherits the (non-admin) proxy's privileges, so a service-managed install
|
|
341
|
+
// updated from the GUI or a normal terminal fails here with access denied.
|
|
342
|
+
// Falling back to a direct proxy start keeps the update from leaving the proxy
|
|
343
|
+
// stopped; the stale service manager can be refreshed later with an admin
|
|
344
|
+
// `ocx service install`.
|
|
345
|
+
updateJob(job, {}, `Service reinstall failed (exit ${result.status ?? "?"}); falling back to a direct proxy start. Run 'ocx service install' as administrator to refresh the background service manager.`);
|
|
338
346
|
}
|
|
339
347
|
} finally {
|
|
340
348
|
if (prevBake === undefined) delete process.env.OCX_BAKE_PORT;
|
|
341
349
|
else process.env.OCX_BAKE_PORT = prevBake;
|
|
342
350
|
}
|
|
343
|
-
return;
|
|
351
|
+
if (serviceOk) return;
|
|
352
|
+
// Fall through to the direct proxy start below so the update never leaves the
|
|
353
|
+
// proxy stopped when the service reinstall could not run.
|
|
344
354
|
}
|
|
345
355
|
|
|
346
356
|
const pid = readPid();
|
|
@@ -37,6 +37,7 @@ const KIMI_K27_CODE: Cost4 = { input: 0.95, output: 4, cacheRead: 0.19, cacheWri
|
|
|
37
37
|
const KIMI_K27_CODE_HIGHSPEED: Cost4 = { input: 1.9, output: 8, cacheRead: 0.38, cacheWrite: 1.9 };
|
|
38
38
|
const KIMI_K26: Cost4 = { input: 0.95, output: 4, cacheRead: 0.16, cacheWrite: 0.95 };
|
|
39
39
|
const KIMI_K25: Cost4 = { input: 0.6, output: 3, cacheRead: 0.1, cacheWrite: 0.6 };
|
|
40
|
+
const QWEN38_ROUTEWAY_TEMPORARY: Cost4 = { input: 1.5, output: 5, cacheRead: 0.15, cacheWrite: 0 };
|
|
40
41
|
|
|
41
42
|
const GEMINI_PRICING = "https://ai.google.dev/gemini-api/docs/pricing (2026-07-22); cacheWrite=0: storage is billed per-hour, not per-token";
|
|
42
43
|
const MINIMAX_PRICING = "https://platform.minimax.io/docs/guides/pricing-paygo";
|
|
@@ -44,6 +45,9 @@ const DEEPSEEK_PRICING = "https://api-docs.deepseek.com/quick_start/pricing-deta
|
|
|
44
45
|
// Kimi official tables publish input/output/cache-hit only; cacheWrite is mapped to the
|
|
45
46
|
// cache-miss input price (Kimi auto-caches with no separate write billing). 2026-07-20 re-verified.
|
|
46
47
|
const KIMI_PRICING = "https://platform.kimi.ai/docs/pricing (official table; cacheWrite derived = input, Kimi auto-cache has no write billing)";
|
|
48
|
+
// TEMPORARY proxy only: Routeway's reseller API rate is not Alibaba Token Plan billing.
|
|
49
|
+
// Replace these overlays when Alibaba publishes an official qwen3.8-max-preview token rate.
|
|
50
|
+
const QWEN38_ROUTEWAY_PRICING = "https://routeway.ai/models/qwen3.8-max-preview (temporary reseller proxy; NOT Alibaba Token Plan billing; cacheWrite unpublished -> 0)";
|
|
47
51
|
|
|
48
52
|
export const EXPECTED_PRICE_OVERLAYS: readonly ExpectedPriceOverlay[] = [
|
|
49
53
|
// MiniMax M2.1 highspeed — published PAYG price (verified).
|
|
@@ -55,6 +59,8 @@ export const EXPECTED_PRICE_OVERLAYS: readonly ExpectedPriceOverlay[] = [
|
|
|
55
59
|
// Google Antigravity effort-suffix variants — derived from the verified base-model
|
|
56
60
|
// price (Google does not publish per-suffix prices; Agent inference bills at the
|
|
57
61
|
// base model's standard rate per the official Billing FAQ).
|
|
62
|
+
{ provider: "google-antigravity", modelId: "gemini-3.6-flash", cost4: GEMINI_36_FLASH, source: `collapsed base ID ${GEMINI_PRICING}`, verifiedAt: "2026-07-22", status: "verified" },
|
|
63
|
+
{ provider: "google-antigravity", modelId: "gemini-3.1-pro", cost4: GEMINI_31_PRO, source: `collapsed base ID ${GEMINI_PRICING}`, verifiedAt: "2026-07-22", status: "verified" },
|
|
58
64
|
{ provider: "google-antigravity", modelId: "gemini-3.1-pro-low", cost4: GEMINI_31_PRO, source: `derived: gemini-3.1-pro (<=200k tier) ${GEMINI_PRICING}`, verifiedAt: "2026-07-20", status: "verified-derived" },
|
|
59
65
|
{ provider: "google-antigravity", modelId: "gemini-3.1-pro-high", cost4: GEMINI_31_PRO, source: `derived: gemini-3.1-pro (<=200k tier) ${GEMINI_PRICING}`, verifiedAt: "2026-07-20", status: "verified-derived" },
|
|
60
66
|
{ provider: "google-antigravity", modelId: "gemini-3.6-flash-low", cost4: GEMINI_36_FLASH, source: `derived: gemini-3.6-flash ${GEMINI_PRICING}`, verifiedAt: "2026-07-22", status: "verified-derived" },
|
|
@@ -95,6 +101,10 @@ export const EXPECTED_PRICE_OVERLAYS: readonly ExpectedPriceOverlay[] = [
|
|
|
95
101
|
{ provider: "kimi-code", modelId: "kimi-k2.6", cost4: KIMI_K26, source: KIMI_PRICING, verifiedAt: "2026-07-20", status: "verified-derived" },
|
|
96
102
|
{ provider: "kimi-code", modelId: "kimi-k2.5", cost4: KIMI_K25, source: KIMI_PRICING, verifiedAt: "2026-07-20", status: "verified-derived" },
|
|
97
103
|
{ provider: "kimi-code", modelId: "kimi-for-coding", cost4: KIMI_K27_CODE, source: `derived: kimi-k2.7-code ${KIMI_PRICING}`, verifiedAt: "2026-07-20", status: "verified-derived" },
|
|
104
|
+
// Alibaba has not published a per-token Token Plan rate yet. Use Routeway's
|
|
105
|
+
// independently published reseller rate temporarily and keep estimates derived.
|
|
106
|
+
{ provider: "alibaba-token-plan", modelId: "qwen3.8-max-preview", cost4: QWEN38_ROUTEWAY_TEMPORARY, source: QWEN38_ROUTEWAY_PRICING, verifiedAt: "2026-07-22", status: "verified-derived" },
|
|
107
|
+
{ provider: "alibaba-token-plan-intl", modelId: "qwen3.8-max-preview", cost4: QWEN38_ROUTEWAY_TEMPORARY, source: QWEN38_ROUTEWAY_PRICING, verifiedAt: "2026-07-22", status: "verified-derived" },
|
|
98
108
|
// Cursor Auto router — Cursor's published fixed token price (verified).
|
|
99
109
|
{ provider: "cursor", modelId: "auto", cost4: { input: 1.25, output: 6, cacheRead: 0.25, cacheWrite: 1.25 }, source: "https://docs.cursor.com/account/pricing + https://cursor.com/blog/aug-2025-pricing", verifiedAt: "2026-07-20", status: "verified" },
|
|
100
110
|
];
|
package/src/usage/summary.ts
CHANGED
|
@@ -63,6 +63,7 @@ export interface UsageModel {
|
|
|
63
63
|
inputTokens: number;
|
|
64
64
|
outputTokens: number;
|
|
65
65
|
shareRatio: number;
|
|
66
|
+
estimatedCostUsd?: number;
|
|
66
67
|
}
|
|
67
68
|
|
|
68
69
|
export interface UsageProvider {
|
|
@@ -74,6 +75,7 @@ export interface UsageProvider {
|
|
|
74
75
|
estimatedRequests: number;
|
|
75
76
|
totalTokens: number;
|
|
76
77
|
shareRatio: number;
|
|
78
|
+
estimatedCostUsd?: number;
|
|
77
79
|
}
|
|
78
80
|
|
|
79
81
|
export interface UsageSummary {
|
|
@@ -350,6 +352,29 @@ function buildModels(entries: PersistedUsageEntry[], totalTokens: number): Usage
|
|
|
350
352
|
else if (status === "estimated") model.estimatedRequests += 1;
|
|
351
353
|
}
|
|
352
354
|
}
|
|
355
|
+
// Accumulate per-model estimated cost
|
|
356
|
+
for (const entry of entries) {
|
|
357
|
+
const estimate = entry.attempts?.length
|
|
358
|
+
? estimateComboCost(entry.attempts)
|
|
359
|
+
: estimateRequestCost({ provider: entry.provider, model: entry.model, usage: entry.usage, usageStatus: entry.usageStatus });
|
|
360
|
+
if (!estimate) continue;
|
|
361
|
+
|
|
362
|
+
if (entry.attempts?.length && estimate.attempts) {
|
|
363
|
+
// Combo: attribute each attempt's cost to its own model
|
|
364
|
+
for (const attemptEst of estimate.attempts) {
|
|
365
|
+
const aProviderKey = baseProviderLabel(attemptEst.provider);
|
|
366
|
+
const aKey = `${aProviderKey}${attemptEst.model}`;
|
|
367
|
+
const m = byKey.get(aKey);
|
|
368
|
+
if (m) m.estimatedCostUsd = (m.estimatedCostUsd ?? 0) + attemptEst.cost.total;
|
|
369
|
+
}
|
|
370
|
+
} else {
|
|
371
|
+
// Single-target: attribute to the entry's model
|
|
372
|
+
const providerKey = baseProviderLabel(entry.provider);
|
|
373
|
+
const key = `${providerKey}${entry.model}`;
|
|
374
|
+
const m = byKey.get(key);
|
|
375
|
+
if (m) m.estimatedCostUsd = (m.estimatedCostUsd ?? 0) + estimate.cost.total;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
353
378
|
const models = [...byKey.values()];
|
|
354
379
|
for (const m of models) m.shareRatio = totalTokens === 0 ? 0 : m.totalTokens / totalTokens;
|
|
355
380
|
return models.sort((a, b) => b.requests - a.requests);
|
|
@@ -396,6 +421,24 @@ function buildProviders(entries: PersistedUsageEntry[], totalTokens: number): Us
|
|
|
396
421
|
else if (status === "estimated") provider.estimatedRequests += 1;
|
|
397
422
|
}
|
|
398
423
|
}
|
|
424
|
+
for (const entry of entries) {
|
|
425
|
+
const estimate = entry.attempts?.length
|
|
426
|
+
? estimateComboCost(entry.attempts)
|
|
427
|
+
: estimateRequestCost({ provider: entry.provider, model: entry.model, usage: entry.usage, usageStatus: entry.usageStatus });
|
|
428
|
+
if (!estimate) continue;
|
|
429
|
+
|
|
430
|
+
if (entry.attempts?.length && estimate.attempts) {
|
|
431
|
+
for (const attemptEst of estimate.attempts) {
|
|
432
|
+
const aProviderKey = baseProviderLabel(attemptEst.provider);
|
|
433
|
+
const p = byKey.get(aProviderKey);
|
|
434
|
+
if (p) p.estimatedCostUsd = (p.estimatedCostUsd ?? 0) + attemptEst.cost.total;
|
|
435
|
+
}
|
|
436
|
+
} else {
|
|
437
|
+
const providerKey = baseProviderLabel(entry.provider);
|
|
438
|
+
const p = byKey.get(providerKey);
|
|
439
|
+
if (p) p.estimatedCostUsd = (p.estimatedCostUsd ?? 0) + estimate.cost.total;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
399
442
|
const providers = [...byKey.values()];
|
|
400
443
|
for (const p of providers) p.shareRatio = totalTokens === 0 ? 0 : p.totalTokens / totalTokens;
|
|
401
444
|
return providers.sort((a, b) => b.requests - a.requests);
|