@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.
Files changed (46) hide show
  1. package/README.ja.md +438 -0
  2. package/README.ko.md +1 -1
  3. package/README.md +1 -1
  4. package/README.ru.md +2 -1
  5. package/README.zh-CN.md +1 -1
  6. package/bin/ocx.mjs +18 -1
  7. package/gui/dist/assets/index-D6Fcl4yM.css +1 -0
  8. package/gui/dist/assets/index-d63HMU0x.js +52 -0
  9. package/gui/dist/index.html +2 -2
  10. package/package.json +1 -1
  11. package/src/adapters/anthropic.ts +17 -2
  12. package/src/adapters/google-tool-schema.ts +4 -0
  13. package/src/adapters/google.ts +10 -2
  14. package/src/adapters/openai-chat.ts +36 -1
  15. package/src/adapters/openai-responses.ts +2 -1
  16. package/src/bridge.ts +12 -4
  17. package/src/cli/account-api.ts +4 -2
  18. package/src/cli/account-extended.ts +34 -0
  19. package/src/cli/account.ts +3 -1
  20. package/src/cli/claude.ts +6 -1
  21. package/src/cli/help.ts +25 -4
  22. package/src/cli/init.ts +38 -3
  23. package/src/cli/models.ts +206 -7
  24. package/src/codex/auth-api.ts +27 -3
  25. package/src/codex/catalog.ts +72 -2
  26. package/src/config.ts +60 -3
  27. package/src/oauth/github-copilot.ts +1 -0
  28. package/src/oauth/index.ts +5 -4
  29. package/src/oauth/kiro.ts +12 -1
  30. package/src/oauth/store.ts +11 -0
  31. package/src/oauth/types.ts +3 -1
  32. package/src/providers/antigravity-models.ts +103 -5
  33. package/src/providers/api-keys.ts +12 -0
  34. package/src/providers/openrouter-routing.ts +102 -0
  35. package/src/providers/registry.ts +7 -5
  36. package/src/router.ts +2 -1
  37. package/src/server/auth-cors.ts +5 -0
  38. package/src/server/management-api.ts +143 -6
  39. package/src/server/responses.ts +16 -4
  40. package/src/types.ts +42 -1
  41. package/src/update/index.ts +19 -2
  42. package/src/update/job.ts +13 -3
  43. package/src/usage/expected-prices.ts +10 -0
  44. package/src/usage/summary.ts +43 -0
  45. package/gui/dist/assets/index-BPa0R6EN.js +0 -46
  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;
@@ -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) console.warn("⚠️ Service refresh failed — run 'ocx service install' manually.");
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
- if (result.status !== 0) {
337
- throw new Error(`service restart failed (${cmd.display}, exit ${result.status ?? "?"})`);
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
  ];
@@ -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);