@bitkyc08/opencodex 2.39.0 → 2.40.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 +49 -21
- package/gui/dist/assets/index-BHe2rl_C.js +112 -0
- package/gui/dist/assets/index-CJSb3HPe.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/cursor/catalog.ts +140 -7
- package/src/adapters/cursor/discovery.ts +79 -87
- package/src/adapters/cursor/effort-map.ts +11 -0
- package/src/adapters/cursor/request-builder.ts +31 -4
- package/src/adapters/cursor.ts +17 -0
- package/src/adapters/openai-chat.ts +95 -9
- package/src/adapters/openai-responses.ts +73 -9
- package/src/bridge.ts +58 -13
- package/src/claude/auth-mode.ts +6 -9
- package/src/claude/gateway-cache.ts +23 -3
- package/src/claude/model-info.ts +13 -2
- package/src/cli/access.ts +30 -0
- package/src/cli/account-extended.ts +19 -10
- package/src/cli/capabilities.ts +40 -1
- package/src/cli/claude-agent-startup-sync.ts +3 -0
- package/src/cli/claude.ts +134 -36
- package/src/cli/connect.ts +226 -0
- package/src/cli/dispatch.ts +126 -21
- package/src/cli/gui-pair-client.ts +170 -0
- package/src/cli/gui.ts +87 -0
- package/src/cli/help.ts +4 -1
- package/src/cli/index.ts +76 -22
- package/src/cli/models-runtime-subcommands.ts +34 -0
- package/src/cli/models-runtime.ts +4 -0
- package/src/cli/models.ts +2 -1
- package/src/cli/opencode.ts +2 -1
- package/src/cli/provider-runtime.ts +31 -1
- package/src/cli/registry.ts +32 -2
- package/src/cli/runtime-api.ts +10 -1
- package/src/cli/status.ts +26 -0
- package/src/cli/system-command.ts +9 -3
- package/src/client/connect.ts +650 -0
- package/src/client/hub-client.ts +457 -0
- package/src/client/hub-relay.ts +288 -0
- package/src/client/machine-api.ts +139 -0
- package/src/client/machine-auth.ts +54 -0
- package/src/client/machine-listener.ts +143 -0
- package/src/client/runtime.ts +93 -0
- package/src/client/state.ts +175 -0
- package/src/clients/config-export.ts +16 -5
- package/src/codex/auth-api.ts +169 -15
- package/src/codex/auth-context.ts +61 -6
- package/src/codex/catalog/aggregation.ts +17 -13
- package/src/codex/catalog/metadata.ts +28 -0
- package/src/codex/catalog/provider-fetch.ts +38 -5
- package/src/codex/catalog.ts +1 -1
- package/src/codex/desired-state.ts +13 -3
- package/src/codex/inject.ts +212 -31
- package/src/codex/journal.ts +54 -6
- package/src/codex/main-account.ts +105 -33
- package/src/codex/native-main-claim.ts +23 -1
- package/src/codex/native-residue.ts +7 -0
- package/src/codex/reset-credit-auto-redeem.ts +237 -0
- package/src/combos/request.ts +15 -3
- package/src/combos/types.ts +12 -0
- package/src/config/paths.ts +15 -1
- package/src/config/provider-validation.ts +40 -0
- package/src/config.ts +447 -3
- package/src/generated/compatibility-version.json +224 -108
- package/src/images/artifacts.ts +73 -37
- package/src/images/fulfill.ts +6 -1
- package/src/images/index.ts +1 -1
- package/src/images/plan.ts +16 -2
- package/src/images/synthetic-tool.ts +5 -0
- package/src/images/xai-client.ts +34 -1
- package/src/integrations/cursor-detect.ts +133 -0
- package/src/integrations/cursor-seen.ts +31 -0
- package/src/lib/errors.ts +18 -0
- package/src/lib/gui-pair-capability.ts +104 -0
- package/src/lib/lab-live-route-production.ts +2 -1
- package/src/lib/process-control.ts +4 -3
- package/src/lib/service-secrets.ts +161 -1
- package/src/lib/windows-system-proxy.ts +115 -0
- package/src/oauth/index.ts +3 -2
- package/src/oauth/pool-settings-capability.ts +55 -0
- package/src/providers/api-keys.ts +3 -1
- package/src/providers/codex-capacity.ts +34 -4
- package/src/providers/derive.ts +10 -0
- package/src/providers/fastwire.ts +10 -2
- package/src/providers/key-store.ts +197 -0
- package/src/providers/model-rename-migration.ts +3 -0
- package/src/providers/openai-sidecar.ts +3 -3
- package/src/providers/quota.ts +91 -42
- package/src/providers/registry.ts +33 -3
- package/src/reasoning-effort.ts +32 -0
- package/src/remote/protocol.ts +109 -0
- package/src/responses/citation-markers.ts +101 -0
- package/src/responses/parser.ts +43 -3
- package/src/responses/spill-store.ts +9 -4
- package/src/router.ts +72 -8
- package/src/server/auth-cors.ts +37 -1
- package/src/server/gui-session.ts +449 -0
- package/src/server/gui-static.ts +24 -7
- package/src/server/images.ts +237 -10
- package/src/server/index.ts +356 -30
- package/src/server/management/agent-settings-routes.ts +7 -1
- package/src/server/management/api-key-rotation.ts +74 -0
- package/src/server/management/combo-routes.ts +18 -6
- package/src/server/management/config-routes.ts +22 -3
- package/src/server/management/context.ts +5 -1
- package/src/server/management/cursor-integration-routes.ts +98 -0
- package/src/server/management/logs-usage-routes.ts +2 -1
- package/src/server/management/model-routes.ts +79 -1
- package/src/server/management/model-rows.ts +23 -1
- package/src/server/management/oauth-account-routes.ts +135 -2
- package/src/server/management/provider-routes.ts +143 -9
- package/src/server/management/route-registry.ts +10 -0
- package/src/server/management/session-routes.ts +13 -0
- package/src/server/management/system-routes.ts +13 -1
- package/src/server/management-api.ts +8 -3
- package/src/server/management-auth.ts +124 -82
- package/src/server/models-capabilities.ts +124 -0
- package/src/server/ports.ts +7 -0
- package/src/server/proxy-liveness.ts +7 -0
- package/src/server/request-log.ts +11 -5
- package/src/server/responses/agent-task-recovery.ts +1 -0
- package/src/server/responses/codex-auth-error.ts +3 -1
- package/src/server/responses/collaboration.ts +26 -1
- package/src/server/responses/combo-stream-preflight.ts +25 -6
- package/src/server/responses/compact.ts +32 -12
- package/src/server/responses/context-overflow.ts +49 -0
- package/src/server/responses/core.ts +196 -12
- package/src/server/responses/fetch-helpers.ts +2 -1
- package/src/server/responses/outbound-body-guard.ts +110 -0
- package/src/server/responses/ws-upstream.ts +87 -6
- package/src/server/responses-self-named-namespace-scrub.ts +181 -0
- package/src/server/system-env.ts +78 -25
- package/src/service.ts +218 -42
- package/src/types/config.ts +134 -0
- package/src/types/provider.ts +54 -1
- package/src/types/request.ts +2 -0
- package/src/types.ts +5 -0
- package/src/update/job.ts +33 -5
- package/src/usage/cost.ts +32 -1
- package/src/usage/expected-prices.ts +16 -0
- package/src/usage/log.ts +6 -2
- package/src/usage/summary.ts +48 -6
- package/gui/dist/assets/index-D-lchsPw.js +0 -112
- package/gui/dist/assets/index-uvENYLin.css +0 -1
package/src/types/provider.ts
CHANGED
|
@@ -106,7 +106,13 @@ export interface ProviderRequestPacingConfig extends RequestPacingRule {
|
|
|
106
106
|
}
|
|
107
107
|
|
|
108
108
|
export interface FastWire {
|
|
109
|
-
|
|
109
|
+
/**
|
|
110
|
+
* How the provider expresses Fast on the wire. `service-tier` is OpenAI's
|
|
111
|
+
* `service_tier` request field; `cursor-variant` is a MODEL-VARIANT switch, because
|
|
112
|
+
* Cursor has no tier field — its fast product is a different model id
|
|
113
|
+
* (`claude-opus-5-thinking-high-fast`) or a `{id:"fast"}` request parameter for Grok.
|
|
114
|
+
*/
|
|
115
|
+
kind: "service-tier" | "anthropic-speed" | "cursor-variant";
|
|
110
116
|
/** Canonical tier name to upstream wire spelling. */
|
|
111
117
|
canonicalToWire: Readonly<Record<string, string>>;
|
|
112
118
|
/** Policy for non-canonical caller-provided tier values. */
|
|
@@ -168,6 +174,8 @@ export interface OcxProviderConfig {
|
|
|
168
174
|
alias?: string;
|
|
169
175
|
/** Native model id -> short, slash-free request alias. */
|
|
170
176
|
modelAliases?: Record<string, string>;
|
|
177
|
+
/** Display-only labels for exact native model ids discovered under this provider. */
|
|
178
|
+
modelDisplayNames?: Record<string, string>;
|
|
171
179
|
/** Override the global built-in model-alias switch for this provider. */
|
|
172
180
|
defaultAliases?: boolean;
|
|
173
181
|
adapter: string;
|
|
@@ -275,6 +283,18 @@ export interface OcxProviderConfig {
|
|
|
275
283
|
* (current behavior unchanged). Only meaningful for https: base URLs.
|
|
276
284
|
*/
|
|
277
285
|
upstreamHttpVersion?: UpstreamHttpVersion;
|
|
286
|
+
/**
|
|
287
|
+
* Opt-in upstream Responses WebSocket transport for `openai-responses` requests. When true,
|
|
288
|
+
* streaming POST turns use the configured Responses path (default `/v1/responses`): forward
|
|
289
|
+
* providers use `{baseUrl}/responses`, while key-auth providers use `responsesPath` or the
|
|
290
|
+
* legacy `/v1/responses` fallback. HTTPS providers use wss and are re-encoded to SSE; HTTP
|
|
291
|
+
* providers continue using SSE, and `openai-chat` requests stay on HTTP. This mirrors the
|
|
292
|
+
* canonical ChatGPT backend optimization for any OpenAI-compatible gateway that speaks the
|
|
293
|
+
* Responses WebSocket protocol (for example an aggregator like sub2api whose WS ingress is
|
|
294
|
+
* measurably faster than its SSE queue). Default false. Canonical ChatGPT backend WS selection
|
|
295
|
+
* is independent of this flag.
|
|
296
|
+
*/
|
|
297
|
+
upstreamWebsocket?: boolean;
|
|
278
298
|
/**
|
|
279
299
|
* Google only. When `false`, the AI Studio (direct) path sends Gemini Flash ids
|
|
280
300
|
* unchanged to the wire instead of applying the `-tiered` suffix (`gemini-3.7-flash`
|
|
@@ -319,6 +339,16 @@ export interface OcxProviderConfig {
|
|
|
319
339
|
* full set so the user can pick). See devlog issue_052_provider-model-allowlist.
|
|
320
340
|
*/
|
|
321
341
|
selectedModels?: string[];
|
|
342
|
+
/**
|
|
343
|
+
* Per-provider retention allowlist for authoritative live discovery. When non-empty, any
|
|
344
|
+
* model id in this list is preserved in the routed catalog even if the live `/models`
|
|
345
|
+
* endpoint omits it (ad-hoc / private providers whose live discovery drops callable ids).
|
|
346
|
+
* Mirrors the built-in `kimi`/`xai` compatibility tables — opt-in for every other provider.
|
|
347
|
+
* Ids listed here need not be repeated in `models`: discovery folds them into the configured
|
|
348
|
+
* seed, so they exist under `liveModels: false` too. `selectedModels` still narrows what is
|
|
349
|
+
* visible. Empty/undefined = no opt-in (default behavior). See #1690.
|
|
350
|
+
*/
|
|
351
|
+
retainModels?: string[];
|
|
322
352
|
/** Override for newly discovered models. Absent/"inherit" uses the install policy. */
|
|
323
353
|
newModelPolicy?: "on" | "off" | "inherit";
|
|
324
354
|
/**
|
|
@@ -402,6 +432,13 @@ export interface OcxProviderConfig {
|
|
|
402
432
|
*/
|
|
403
433
|
oauthAccountFailover?: {
|
|
404
434
|
enabled?: boolean;
|
|
435
|
+
/**
|
|
436
|
+
* Generic OAuth pool selection strategy (#695). Persisted through the pool-settings
|
|
437
|
+
* contract; the selector does not consume it yet, so omitted keeps today's behavior.
|
|
438
|
+
*/
|
|
439
|
+
strategy?: "quota" | "round-robin" | "fill-first";
|
|
440
|
+
/** 0-100 usage percent at which a proactive switch may be considered (#695); inert today. */
|
|
441
|
+
autoSwitchThreshold?: number;
|
|
405
442
|
};
|
|
406
443
|
/** Allow an explicitly key/oauth provider to run without a credential (for keyless local proxies). */
|
|
407
444
|
keyOptional?: boolean;
|
|
@@ -504,6 +541,14 @@ export interface OcxProviderConfig {
|
|
|
504
541
|
* per-model compatibility escape hatch for mixed-capability gateways.
|
|
505
542
|
*/
|
|
506
543
|
noStructuredOutputModels?: string[];
|
|
544
|
+
/**
|
|
545
|
+
* Model ids that accept a reasoning-effort field on an ordinary turn but reject it
|
|
546
|
+
* once function tools are present. The model keeps its advertised effort ladder;
|
|
547
|
+
* OpenCodex omits the wire field for tool-bearing requests only and lets the
|
|
548
|
+
* upstream default apply. Narrower than `noReasoningModels`, which strips reasoning
|
|
549
|
+
* from every request and costs the model its picker entirely.
|
|
550
|
+
*/
|
|
551
|
+
omitReasoningEffortWithToolsModels?: string[];
|
|
507
552
|
/**
|
|
508
553
|
* Allow multiple tool calls per completion. DEFAULT-ON for openai-chat providers (the
|
|
509
554
|
* buffered stream parser assembles interleaved/fragmented multi-call turns safely);
|
|
@@ -596,6 +641,14 @@ export interface OcxProviderConfig {
|
|
|
596
641
|
* thinking separately in `reasoning_content` / `reasoning_details` instead of visible content.
|
|
597
642
|
*/
|
|
598
643
|
reasoningSplitModels?: string[];
|
|
644
|
+
/**
|
|
645
|
+
* Model ids whose chat endpoint carries thinking as a structured `reasoning_details` array
|
|
646
|
+
* (MiniMax M-series with `reasoning_split`): stream deltas repeat each detail's `text` as a
|
|
647
|
+
* cumulative snapshot, so the adapter prefix-diffs instead of appending, and preserved
|
|
648
|
+
* reasoning replays as a `reasoning_details` array rather than a `reasoning_content` string
|
|
649
|
+
* (upstream requires the array back verbatim to keep interleaved thinking intact).
|
|
650
|
+
*/
|
|
651
|
+
reasoningDetailsModels?: string[];
|
|
599
652
|
/**
|
|
600
653
|
* Model ids whose reasoning is a vendor `thinking: {type}` toggle on the
|
|
601
654
|
* chat-completions wire (MiMo v2.x, GLM 5/5.1 style), NOT an OpenAI `reasoning_effort` ladder.
|
package/src/types/request.ts
CHANGED
|
@@ -333,6 +333,8 @@ export type AdapterEvent =
|
|
|
333
333
|
| {
|
|
334
334
|
type: "done";
|
|
335
335
|
usage?: OcxUsage;
|
|
336
|
+
/** Native opaque compaction ciphertext returned by a Responses backend. */
|
|
337
|
+
compactionEncryptedContent?: string;
|
|
336
338
|
stopReason?: string;
|
|
337
339
|
endTurn?: boolean;
|
|
338
340
|
providerState?: OcxProviderContinuationState;
|
package/src/types.ts
CHANGED
|
@@ -63,11 +63,16 @@ export type {
|
|
|
63
63
|
OcxApiKeyEntry,
|
|
64
64
|
OcxClientIntegrationsConfig,
|
|
65
65
|
OcxConfigRebaseProvenance,
|
|
66
|
+
OcxHubConfig,
|
|
67
|
+
OcxRemoteGuiConfig,
|
|
68
|
+
OcxConnectedClientId,
|
|
69
|
+
OcxClientConnectionConfig,
|
|
66
70
|
OcxConfig,
|
|
67
71
|
OcxAccountPoolRotationStrategy,
|
|
68
72
|
OcxAccountPoolQuotaWindow,
|
|
69
73
|
OcxComboStrategy,
|
|
70
74
|
OcxComboDefaultEffort,
|
|
75
|
+
OcxComboReasoningEffortMode,
|
|
71
76
|
OcxComboTarget,
|
|
72
77
|
OcxComboConfig,
|
|
73
78
|
OcxRoutingUnknownEvidenceMode,
|
package/src/update/job.ts
CHANGED
|
@@ -51,6 +51,11 @@ const RELEASE_NOTES_URL = "https://github.com/lidge-jun/opencodex/releases/lates
|
|
|
51
51
|
const UPDATE_JOB_FILENAME = "update-job.json";
|
|
52
52
|
const UPDATE_TIMEOUT_MS = 180_000;
|
|
53
53
|
const RESTART_TIMEOUT_MS = 60_000;
|
|
54
|
+
// A Windows `service repair` can spend up to 45s in its own serving probe after
|
|
55
|
+
// Task Scheduler/ACL work. The generic 60s child ceiling can kill that valid repair
|
|
56
|
+
// and launch a competing foreground proxy. Keep this below the update worker's 180s
|
|
57
|
+
// ceiling while covering the measured probe plus bounded Windows setup work.
|
|
58
|
+
const WINDOWS_SERVICE_REPAIR_TIMEOUT_MS = 150_000;
|
|
54
59
|
const RESTART_HEALTH_TIMEOUT_MS = 30_000;
|
|
55
60
|
const RESTART_STABILITY_WINDOW_MS = 15_000;
|
|
56
61
|
/** Legacy active records did not persist a worker PID, so age is their only safe recovery signal. */
|
|
@@ -687,7 +692,12 @@ export function startUpdateJob(
|
|
|
687
692
|
* and a bounded, structured summary — enough to tell a user which step failed and how, with no
|
|
688
693
|
* free-form vendor text passing through the boundary. Detailed output stays ephemeral.
|
|
689
694
|
*/
|
|
690
|
-
function runLoggedCommand(
|
|
695
|
+
function runLoggedCommand(
|
|
696
|
+
job: UpdateJobState,
|
|
697
|
+
bin: string,
|
|
698
|
+
args: string[],
|
|
699
|
+
timeout: number,
|
|
700
|
+
): { status: number | null; signal: NodeJS.Signals | null; timedOut: boolean } {
|
|
691
701
|
job = updateJob(job, {}, `$ ${formatCommand(bin, args)}`);
|
|
692
702
|
const result = spawnSync(bin, args, {
|
|
693
703
|
encoding: "utf8",
|
|
@@ -698,7 +708,11 @@ function runLoggedCommand(job: UpdateJobState, bin: string, args: string[], time
|
|
|
698
708
|
const stderr = typeof result.stderr === "string" ? result.stderr.trim() : "";
|
|
699
709
|
const summary = summarizeCommandOutput(stdout, stderr, result.status, result.signal);
|
|
700
710
|
if (summary) updateJob(job, {}, summary);
|
|
701
|
-
return {
|
|
711
|
+
return {
|
|
712
|
+
status: result.status,
|
|
713
|
+
signal: result.signal,
|
|
714
|
+
timedOut: (result.error as NodeJS.ErrnoException | undefined)?.code === "ETIMEDOUT",
|
|
715
|
+
};
|
|
702
716
|
}
|
|
703
717
|
|
|
704
718
|
/**
|
|
@@ -987,7 +1001,8 @@ export interface RestartIo {
|
|
|
987
1001
|
job: UpdateJobState,
|
|
988
1002
|
bin: string,
|
|
989
1003
|
args: string[],
|
|
990
|
-
|
|
1004
|
+
timeoutMs: number,
|
|
1005
|
+
) => { status: number | null; signal?: NodeJS.Signals | null; timedOut?: boolean };
|
|
991
1006
|
/** Override the explicit restart path (used by finishGuiUpdateRestart tests). */
|
|
992
1007
|
restartAfterUpdateFn?: (
|
|
993
1008
|
job: UpdateJobState,
|
|
@@ -1155,10 +1170,23 @@ async function restartAfterUpdate(
|
|
|
1155
1170
|
process.env.OCX_BAKE_PORT = String(Math.trunc(port));
|
|
1156
1171
|
let serviceOk = false;
|
|
1157
1172
|
try {
|
|
1158
|
-
const
|
|
1159
|
-
|
|
1173
|
+
const repairTimeoutMs = (io.platform ?? process.platform) === "win32"
|
|
1174
|
+
? WINDOWS_SERVICE_REPAIR_TIMEOUT_MS
|
|
1175
|
+
: RESTART_TIMEOUT_MS;
|
|
1176
|
+
const run = io.runService ?? ((j, bin, args, timeoutMs) => runLoggedCommand(j, bin, args, timeoutMs));
|
|
1177
|
+
const result = run(job, cmd.bin, cmd.args, repairTimeoutMs);
|
|
1160
1178
|
serviceOk = result.status === 0;
|
|
1161
1179
|
if (!serviceOk) {
|
|
1180
|
+
if (result.timedOut) {
|
|
1181
|
+
// UAC and scheduler mutation can outlive a fixed child deadline. Once the
|
|
1182
|
+
// worker kills that child, ownership is ambiguous: launching a foreground
|
|
1183
|
+
// proxy here can race a registration that completes moments later.
|
|
1184
|
+
updateJob(job, {}, "Service repair timed out with Task Scheduler state unknown; refusing a competing direct start.");
|
|
1185
|
+
throw new Error(
|
|
1186
|
+
"Service repair timed out with Task Scheduler state unknown; refusing a competing direct start. "
|
|
1187
|
+
+ "Run 'ocx service status', then 'ocx service repair' by hand.",
|
|
1188
|
+
);
|
|
1189
|
+
}
|
|
1162
1190
|
// The refresh that just failed was `ocx service repair` (serviceReinstallArgs).
|
|
1163
1191
|
// It normally reuses a healthy registration, but a stale definition may have tried
|
|
1164
1192
|
// guarded re-registration/elevation. Advising `install` here would unconditionally
|
package/src/usage/cost.ts
CHANGED
|
@@ -319,7 +319,8 @@ function resolveModelLevelPrice(provider: string, modelId: string): MatchedPrice
|
|
|
319
319
|
// dots where the catalog uses dashes (kiro "claude-opus-4.6" vs anthropic
|
|
320
320
|
// "claude-opus-4-6"). No fuzzy matching beyond this one normalization.
|
|
321
321
|
const found = findVendorCostByModelId(modelId)
|
|
322
|
-
?? (modelId.includes(".") ? findVendorCostByModelId(modelId.replaceAll(".", "-")) : undefined)
|
|
322
|
+
?? (modelId.includes(".") ? findVendorCostByModelId(modelId.replaceAll(".", "-")) : undefined)
|
|
323
|
+
?? vendorPrefixedCost(modelId);
|
|
323
324
|
if (!found) return null;
|
|
324
325
|
return {
|
|
325
326
|
provider,
|
|
@@ -331,6 +332,36 @@ function resolveModelLevelPrice(provider: string, modelId: string): MatchedPrice
|
|
|
331
332
|
};
|
|
332
333
|
}
|
|
333
334
|
|
|
335
|
+
/**
|
|
336
|
+
* Aggregators spell a model as `<vendor>/<model>` — CommandCode serves
|
|
337
|
+
* `deepseek/deepseek-v4-flash`, and OpenRouter-shaped presets do the same. The cost
|
|
338
|
+
* catalog stores the bare id, so the exact lookup above misses a price that is present and
|
|
339
|
+
* every request through such a provider reports no cost at all (#3136).
|
|
340
|
+
*
|
|
341
|
+
* Retrying on the tail is only safe while the prefix AGREES with the vendor the matched row
|
|
342
|
+
* belongs to. `findVendorCostByModelId` returns whichever vendor `COST_VENDOR_PRIORITY`
|
|
343
|
+
* reaches first, so an unchecked strip would happily price `openai/claude-opus-4-6` from
|
|
344
|
+
* Anthropic's row — a number that looks authoritative and is wrong. Requiring agreement
|
|
345
|
+
* keeps the failure closed for a genuinely mismatched id.
|
|
346
|
+
*
|
|
347
|
+
* Comparison is normalized because the same vendor is spelled differently across catalogs:
|
|
348
|
+
* `x-ai/grok-4.6` resolves to vendor `xai`. Dashes and case are the only variance seen;
|
|
349
|
+
* anything beyond that stays a miss.
|
|
350
|
+
*/
|
|
351
|
+
function vendorPrefixedCost(modelId: string): ReturnType<typeof findVendorCostByModelId> {
|
|
352
|
+
const slash = modelId.indexOf("/");
|
|
353
|
+
if (slash <= 0 || slash === modelId.length - 1) return undefined;
|
|
354
|
+
const claimedVendor = modelId.slice(0, slash);
|
|
355
|
+
const tail = modelId.slice(slash + 1);
|
|
356
|
+
// A tail that is itself slashed is not a vendor prefix we understand; leave it alone.
|
|
357
|
+
if (tail.includes("/")) return undefined;
|
|
358
|
+
const found = findVendorCostByModelId(tail)
|
|
359
|
+
?? (tail.includes(".") ? findVendorCostByModelId(tail.replaceAll(".", "-")) : undefined);
|
|
360
|
+
if (!found) return undefined;
|
|
361
|
+
const normalize = (value: string): string => value.toLowerCase().replaceAll("-", "");
|
|
362
|
+
return normalize(found.provider) === normalize(claimedVendor) ? found : undefined;
|
|
363
|
+
}
|
|
364
|
+
|
|
334
365
|
function isEstimated(usage: OcxUsage, usageStatus: UsageStatus, priceStatus: ExpectedPriceStatus | "verified"): boolean {
|
|
335
366
|
return usage.estimated === true || usageStatus === "estimated" || priceStatus === "verified-derived";
|
|
336
367
|
}
|
|
@@ -66,6 +66,10 @@ const QWEN38_MAX: Cost4 = { input: 2, output: 6, cacheRead: 0, cacheWrite: 0 };
|
|
|
66
66
|
// Anthropic official list prices (USD / 1M tokens). Cache write uses the published 5-minute rate.
|
|
67
67
|
const CLAUDE_SONNET_46: Cost4 = { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 };
|
|
68
68
|
const CLAUDE_OPUS_46: Cost4 = { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 };
|
|
69
|
+
// Claude Fable 5.1: 10 / 50, 5m cache write 12.50. Cache hits are 0.025x base input
|
|
70
|
+
// (0.25) on Fable 5.1 — NOT the 0.1x (1.00) that Fable 5 and every other family use;
|
|
71
|
+
// the pricing page footnote calls this out explicitly. Verified 2026-09-02.
|
|
72
|
+
const CLAUDE_FABLE_51: Cost4 = { input: 10, output: 50, cacheRead: 0.25, cacheWrite: 12.5 };
|
|
69
73
|
// Opus 5 is priced from the maintainer's confirmation that it matches the previous
|
|
70
74
|
// Opus, not from a published Opus 5 page. Hence `verified-derived`, and a source
|
|
71
75
|
// string that states the provenance instead of pointing at ANTHROPIC_PRICING.
|
|
@@ -91,6 +95,18 @@ const KIMI_PRICING = "https://platform.kimi.ai/docs/pricing (official table; cac
|
|
|
91
95
|
const QWEN38_MAX_PRICING = "https://qwen.ai/blog?id=qwen3.8 (Qwen release announcement; no Model Studio billing row yet; cache rates unpublished -> 0)";
|
|
92
96
|
|
|
93
97
|
export const EXPECTED_PRICE_OVERLAYS: readonly ExpectedPriceOverlay[] = [
|
|
98
|
+
// claude-fable-5-1 has no jawcode row yet, so both Anthropic surfaces need their own
|
|
99
|
+
// overlay (the overlay lookup is keyed by the configured provider id; only the jawcode
|
|
100
|
+
// bundle collapses anthropic-apikey onto anthropic).
|
|
101
|
+
{ provider: "anthropic", modelId: "claude-fable-5-1", cost4: CLAUDE_FABLE_51, source: `anthropic official Claude Fable 5.1 ${ANTHROPIC_PRICING}; cache hit = 0.025x base input`, verifiedAt: "2026-09-02", status: "verified" },
|
|
102
|
+
{ provider: "anthropic-apikey", modelId: "claude-fable-5-1", cost4: CLAUDE_FABLE_51, source: `anthropic official Claude Fable 5.1 ${ANTHROPIC_PRICING}; cache hit = 0.025x base input`, verifiedAt: "2026-09-02", status: "verified" },
|
|
103
|
+
// Cursor seeds Fable 5.1 preemptively under three spellings (adapters/cursor/catalog.ts);
|
|
104
|
+
// the model-level vendor fallback only searches jawcode metadata, which has no Fable 5.1
|
|
105
|
+
// row yet, so each Cursor spelling needs its own overlay. Vendor list price, like the
|
|
106
|
+
// cursor/claude-opus-5 row.
|
|
107
|
+
{ provider: "cursor", modelId: "claude-fable-5-1", cost4: CLAUDE_FABLE_51, source: `anthropic official Claude Fable 5.1 ${ANTHROPIC_PRICING}; cache hit = 0.025x base input; vendor list price applied to the Cursor surface`, verifiedAt: "2026-09-02", status: "verified-derived" },
|
|
108
|
+
{ provider: "cursor", modelId: "claude-fable-5.1", cost4: CLAUDE_FABLE_51, source: `anthropic official Claude Fable 5.1 ${ANTHROPIC_PRICING}; cache hit = 0.025x base input; vendor list price applied to the Cursor surface`, verifiedAt: "2026-09-02", status: "verified-derived" },
|
|
109
|
+
{ provider: "cursor", modelId: "claude-5.1-fable", cost4: CLAUDE_FABLE_51, source: `anthropic official Claude Fable 5.1 ${ANTHROPIC_PRICING}; cache hit = 0.025x base input; vendor list price applied to the Cursor surface`, verifiedAt: "2026-09-02", status: "verified-derived" },
|
|
94
110
|
// claude-opus-5 is exposed by three providers but absent from the jawcode bundle, so
|
|
95
111
|
// cost resolution returned null and the Logs `~$` column rendered an em dash. The
|
|
96
112
|
// model-level vendor fallback only searches jawcode metadata, never overlays, so one
|
package/src/usage/log.ts
CHANGED
|
@@ -322,7 +322,8 @@ function normalizeAttemptTierOutcome(raw: unknown): AttemptTierOutcome | null {
|
|
|
322
322
|
if ("wireKind" in outcome
|
|
323
323
|
&& outcome.wireKind !== null
|
|
324
324
|
&& outcome.wireKind !== "service-tier"
|
|
325
|
-
&& outcome.wireKind !== "anthropic-speed"
|
|
325
|
+
&& outcome.wireKind !== "anthropic-speed"
|
|
326
|
+
&& outcome.wireKind !== "cursor-variant") return null;
|
|
326
327
|
if ("wireValue" in outcome && outcome.wireValue !== null && typeof outcome.wireValue !== "string") return null;
|
|
327
328
|
if ("fastDowngradeReason" in outcome
|
|
328
329
|
&& (typeof outcome.fastDowngradeReason !== "string"
|
|
@@ -337,7 +338,10 @@ function normalizeAttemptTierOutcome(raw: unknown): AttemptTierOutcome | null {
|
|
|
337
338
|
const responseServiceTier = sanitizeLogMetadataString(outcome.responseServiceTier);
|
|
338
339
|
return {
|
|
339
340
|
...(outcome.canonical === "priority" ? { canonical: "priority" as const } : {}),
|
|
340
|
-
...(outcome.wireKind === null
|
|
341
|
+
...(outcome.wireKind === null
|
|
342
|
+
|| outcome.wireKind === "service-tier"
|
|
343
|
+
|| outcome.wireKind === "anthropic-speed"
|
|
344
|
+
|| outcome.wireKind === "cursor-variant"
|
|
341
345
|
? { wireKind: outcome.wireKind }
|
|
342
346
|
: {}),
|
|
343
347
|
...(outcome.wireValue === null
|
package/src/usage/summary.ts
CHANGED
|
@@ -156,6 +156,7 @@ export interface UsageSummary {
|
|
|
156
156
|
export interface UsageFilterEcho {
|
|
157
157
|
provider: string | null;
|
|
158
158
|
model: string | null;
|
|
159
|
+
apiKeyId: string | null;
|
|
159
160
|
matched: boolean;
|
|
160
161
|
/**
|
|
161
162
|
* True when a retained row came from a combo attribution. Cost partitions
|
|
@@ -1129,6 +1130,11 @@ function normalizeFilterValue(input: string | null | undefined): string | null {
|
|
|
1129
1130
|
return trimmed === "" ? null : trimmed.toLowerCase();
|
|
1130
1131
|
}
|
|
1131
1132
|
|
|
1133
|
+
function normalizeExactFilterValue(input: string | null | undefined): string | null {
|
|
1134
|
+
const trimmed = typeof input === "string" ? input.trim() : "";
|
|
1135
|
+
return trimmed === "" ? null : trimmed;
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1132
1138
|
/**
|
|
1133
1139
|
* Narrow an already-summarised window to one provider and/or model.
|
|
1134
1140
|
*
|
|
@@ -1152,23 +1158,51 @@ function normalizeFilterValue(input: string | null | undefined): string | null {
|
|
|
1152
1158
|
*/
|
|
1153
1159
|
export function projectUsageSummary<T extends UsageSummary>(
|
|
1154
1160
|
summary: T,
|
|
1155
|
-
filter: { provider?: string | null; model?: string | null },
|
|
1161
|
+
filter: { provider?: string | null; model?: string | null; apiKeyId?: string | null },
|
|
1156
1162
|
entries?: PersistedUsageEntry[],
|
|
1157
1163
|
): T & { filter?: UsageFilterEcho } {
|
|
1158
1164
|
const provider = normalizeFilterValue(filter.provider);
|
|
1159
1165
|
const model = normalizeFilterValue(filter.model);
|
|
1160
|
-
|
|
1166
|
+
const apiKeyId = normalizeExactFilterValue(filter.apiKeyId);
|
|
1167
|
+
if (provider === null && model === null && apiKeyId === null) return summary;
|
|
1168
|
+
|
|
1169
|
+
// Re-summarise from the entries the summary was built from, rather than
|
|
1170
|
+
// projecting over its rows.
|
|
1171
|
+
//
|
|
1172
|
+
// Projecting rows looked cheaper and was wrong in three ways that only show
|
|
1173
|
+
// up together: breakdown rows past MAX_USAGE_MODEL_BREAKDOWN_ROWS are
|
|
1174
|
+
// collapsed into a synthetic "other" row, so a provider living only in that
|
|
1175
|
+
// tail is unfindable and reports matched:false despite real usage; a
|
|
1176
|
+
// provider row is a whole-provider aggregate, so a model filter kept the
|
|
1177
|
+
// provider's OTHER models in providers[] while models[] and the totals
|
|
1178
|
+
// excluded them, contradicting itself inside one response; and a model row
|
|
1179
|
+
// carries a single optional cost, so priced/unpriced/unmetered counts could
|
|
1180
|
+
// only be guessed per model rather than counted per request.
|
|
1181
|
+
//
|
|
1182
|
+
// Key ownership is the outer slice: no provider/model attribution or bucket
|
|
1183
|
+
// construction may observe rows belonging to another client key.
|
|
1184
|
+
const keyFilteredEntries = apiKeyId === null
|
|
1185
|
+
? entries ?? []
|
|
1186
|
+
: (entries ?? []).filter(entry => entry.apiKeyId === apiKeyId);
|
|
1161
1187
|
|
|
1188
|
+
// The entries are already in hand on every path that filters, so the honest
|
|
1189
|
+
// computation is also the simple one.
|
|
1162
1190
|
const matches = (rowProvider: string, rowModel: string): boolean => {
|
|
1163
1191
|
if (provider !== null && baseProviderLabel(rowProvider).toLowerCase() !== provider) return false;
|
|
1164
1192
|
if (model !== null && rowModel.toLowerCase() !== model) return false;
|
|
1165
1193
|
return true;
|
|
1166
1194
|
};
|
|
1167
1195
|
|
|
1168
|
-
|
|
1196
|
+
// Narrow to matching ATTRIBUTIONS, not matching entries.
|
|
1197
|
+
//
|
|
1198
|
+
// Keeping a whole combo entry because one of its attempts matched drags the
|
|
1199
|
+
// other attempts' tokens and cost into the filtered totals: a two-attempt
|
|
1200
|
+
// combo filtered to its cheap model reported the expensive model's spend
|
|
1201
|
+
// too. Rewriting the entry down to its matching attempts is what makes the
|
|
1202
|
+
// filtered numbers mean what the flag says.
|
|
1169
1203
|
let comboOverlap = false;
|
|
1170
1204
|
const filtered: PersistedUsageEntry[] = [];
|
|
1171
|
-
for (const entry of
|
|
1205
|
+
for (const entry of keyFilteredEntries) {
|
|
1172
1206
|
if (!entry.attempts?.length) {
|
|
1173
1207
|
const identity = usageModelIdentity(entry.provider, entry.model, entry.resolvedModel);
|
|
1174
1208
|
if (matches(entry.provider, identity.model)) filtered.push(entry);
|
|
@@ -1194,7 +1228,15 @@ export function projectUsageSummary<T extends UsageSummary>(
|
|
|
1194
1228
|
days: projected.days.map(day => ({ ...day, models: day.models.filter(row => matches(row.provider, row.model)) })),
|
|
1195
1229
|
models,
|
|
1196
1230
|
providers: projected.providers.filter(row => retainedProviders.has(row.provider)),
|
|
1197
|
-
|
|
1198
|
-
|
|
1231
|
+
// Account rows are not provider-partitioned in a way this projection could
|
|
1232
|
+
// honestly re-derive, and unfiltered account totals sitting beside filtered
|
|
1233
|
+
// model totals would invite exactly the wrong reading — so a provider or model
|
|
1234
|
+
// filter drops them.
|
|
1235
|
+
//
|
|
1236
|
+
// An apiKeyId-only filter is different: it selects whole entries, so the account
|
|
1237
|
+
// rows projected from those entries are exactly the accounts that key used. They
|
|
1238
|
+
// are honest under that filter and are kept.
|
|
1239
|
+
accounts: provider === null && model === null ? projected.accounts : [],
|
|
1240
|
+
filter: { provider, model, apiKeyId, matched, comboOverlap },
|
|
1199
1241
|
};
|
|
1200
1242
|
}
|