@bitkyc08/opencodex 2.37.0 → 2.38.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/bin/ocx.mjs +69 -10
- package/gui/dist/assets/{index-CowztZdo.js → index-C14iCj_Q.js} +13 -13
- package/gui/dist/assets/index-D7PIz7_g.css +1 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/aside.svg +3 -0
- package/gui/dist/provider-icons/deepseek-harness.svg +3 -0
- package/gui/dist/provider-icons/oh-my-pi.svg +11 -0
- package/gui/dist/provider-icons/openclaw.svg +54 -0
- package/gui/dist/provider-icons/prime-agent.svg +21 -0
- package/gui/dist/provider-icons/zcode.svg +219 -0
- package/package.json +1 -1
- package/src/adapters/cursor/protobuf-request.ts +4 -1
- package/src/adapters/cursor/tool-definitions.ts +36 -4
- package/src/cli/capabilities.ts +14 -0
- package/src/cli/codex-cli-update.ts +96 -0
- package/src/cli/codex-shim-autorestore.ts +3 -0
- package/src/cli/export-command.ts +18 -17
- package/src/cli/help.ts +2 -2
- package/src/cli/index.ts +3 -2
- package/src/cli/launcher-context.ts +53 -2
- package/src/cli/opencode.ts +126 -33
- package/src/cli/registry.ts +16 -10
- package/src/cli/system-command.ts +6 -1
- package/src/clients/config-export.ts +293 -28
- package/src/codex/account-store.ts +10 -4
- package/src/codex/autostart-health.ts +3 -3
- package/src/codex/catalog/provider-fetch.ts +20 -1
- package/src/codex/catalog/sync.ts +4 -3
- package/src/codex/cli-install-provenance.ts +795 -0
- package/src/codex/convergence.ts +4 -3
- package/src/codex/credential-mutation-epoch.ts +11 -0
- package/src/codex/main-account.ts +2 -0
- package/src/codex/model-entitlements.ts +430 -27
- package/src/codex/native-profile-manager.ts +4 -0
- package/src/codex/reset-credit-operation-ledger.ts +1411 -0
- package/src/codex/reset-credit-recovery.ts +20 -2
- package/src/codex/shim.ts +204 -18
- package/src/codex/user-identity.ts +2 -1
- package/src/config/paths.ts +18 -3
- package/src/config.ts +23 -0
- package/src/generated/compatibility-version.json +81 -45
- package/src/integrations/registry.ts +112 -0
- package/src/integrations/state.ts +67 -5
- package/src/integrations/writer.ts +25 -9
- package/src/lib/bounded-subprocess.ts +36 -0
- package/src/lib/strict-semver.ts +47 -0
- package/src/lib/windows-elevation.ts +32 -1
- package/src/lib/windows-secret-acl.ts +47 -25
- package/src/lib/windows-service-mutation-lock.ts +133 -0
- package/src/lib/windows-user-principal.ts +15 -17
- package/src/responses/spill-store.ts +334 -29
- package/src/responses/state.ts +488 -7
- package/src/server/index.ts +4 -3
- package/src/server/lifecycle.ts +5 -1
- package/src/server/management/model-rows.ts +11 -2
- package/src/server/management/provider-routes.ts +4 -0
- package/src/server/management/system-restart.ts +5 -5
- package/src/server/management-api.ts +7 -2
- package/src/server/startup-action-control.ts +3 -2
- package/src/service.ts +594 -33
- package/src/sidecar/candidates.ts +1 -1
- package/src/update/codex-cli-update-launch-policy.d.mts +18 -0
- package/src/update/codex-cli-update-launch-policy.mjs +30 -0
- package/src/update/index.ts +3 -2
- package/src/update/job.ts +10 -11
- package/gui/dist/assets/index-jqE_VOKI.css +0 -1
|
@@ -20,12 +20,12 @@
|
|
|
20
20
|
* targeting it is the caller's explicit act.
|
|
21
21
|
*/
|
|
22
22
|
import { homedir } from "node:os";
|
|
23
|
-
import { existsSync } from "node:fs";
|
|
23
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
24
24
|
import { isAbsolute, join, resolve } from "node:path";
|
|
25
25
|
import { shouldInjectApiAuthHeader } from "../codex/inject";
|
|
26
26
|
import { FORMAT_MEDIA_TYPE, serializeDocument, type ConfigFormat } from "../integrations/serialize";
|
|
27
27
|
import { providerCodexAccountMode } from "../providers/registry";
|
|
28
|
-
import { sanitizeCodexReasoningEfforts } from "../reasoning-effort";
|
|
28
|
+
import { canonicalizeReasoningEfforts, sanitizeCodexReasoningEfforts } from "../reasoning-effort";
|
|
29
29
|
import { probeHostname } from "../server/proxy-liveness";
|
|
30
30
|
import type { OcxConfig } from "../types";
|
|
31
31
|
|
|
@@ -65,6 +65,14 @@ export interface OpencodeCatalogModel {
|
|
|
65
65
|
id?: string;
|
|
66
66
|
contextWindow?: number;
|
|
67
67
|
displayName?: string;
|
|
68
|
+
/** Declared effort ladder. Exported as opencode model variants where the client reads them. */
|
|
69
|
+
reasoningEfforts?: readonly string[];
|
|
70
|
+
/**
|
|
71
|
+
* Declared default effort. Carried so every client export reads one deduped, visibility-
|
|
72
|
+
* filtered ladder per model. The opencode serializer deliberately does NOT turn it into a
|
|
73
|
+
* model-level setting — see {@link opencodeEffortVariants} for why.
|
|
74
|
+
*/
|
|
75
|
+
defaultReasoningEffort?: string;
|
|
68
76
|
}
|
|
69
77
|
|
|
70
78
|
export interface OpencodeModelEntry {
|
|
@@ -72,20 +80,61 @@ export interface OpencodeModelEntry {
|
|
|
72
80
|
limit?: { context: number; output: number };
|
|
73
81
|
}
|
|
74
82
|
|
|
83
|
+
/**
|
|
84
|
+
* One selectable reasoning effort.
|
|
85
|
+
*
|
|
86
|
+
* opencode V2 applies these only from the `providers` block: a `variants` array under the
|
|
87
|
+
* legacy `provider` block is parsed and then dropped, so the V1 block stays variant-free
|
|
88
|
+
* rather than carrying fields that look configured but never reach a request.
|
|
89
|
+
*/
|
|
90
|
+
export interface OpencodeModelVariant {
|
|
91
|
+
id: string;
|
|
92
|
+
settings: { reasoningEffort: string };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface OpencodeV2ModelEntry extends OpencodeModelEntry {
|
|
96
|
+
variants?: OpencodeModelVariant[];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Endpoint and admission, spelled once and shared by both block generations. */
|
|
100
|
+
export interface OpencodeProviderConnection {
|
|
101
|
+
baseURL: string;
|
|
102
|
+
apiKey?: string;
|
|
103
|
+
headers?: Record<string, string>;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** opencode V1 provider block: `npm` + `options`. */
|
|
75
107
|
export interface OpencodeProviderBlock {
|
|
76
108
|
npm: string;
|
|
77
109
|
name: string;
|
|
78
|
-
options:
|
|
79
|
-
baseURL: string;
|
|
80
|
-
apiKey?: string;
|
|
81
|
-
headers?: Record<string, string>;
|
|
82
|
-
};
|
|
110
|
+
options: OpencodeProviderConnection;
|
|
83
111
|
models: Record<string, OpencodeModelEntry>;
|
|
84
112
|
}
|
|
85
113
|
|
|
114
|
+
/** opencode V2 provider block: `package` + `settings`. The only form whose variants apply. */
|
|
115
|
+
export interface OpencodeV2ProviderBlock {
|
|
116
|
+
package: string;
|
|
117
|
+
name: string;
|
|
118
|
+
settings: OpencodeProviderConnection;
|
|
119
|
+
models: Record<string, OpencodeV2ModelEntry>;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Both generations, always built together: they are one document's two fragments and must
|
|
124
|
+
* agree on the model set, the names, and the connection. Building them in one pass is what
|
|
125
|
+
* makes that a fact rather than a convention.
|
|
126
|
+
*/
|
|
127
|
+
export interface OpencodeProviderBlocks {
|
|
128
|
+
v1: OpencodeProviderBlock;
|
|
129
|
+
v2: OpencodeV2ProviderBlock;
|
|
130
|
+
}
|
|
131
|
+
|
|
86
132
|
export interface OpencodeGeneratedConfig {
|
|
87
133
|
$schema: string;
|
|
134
|
+
/** Legacy block. Kept so opencode V1 installs keep working; V2 merges both and this one loses. */
|
|
88
135
|
provider: Record<string, OpencodeProviderBlock>;
|
|
136
|
+
/** opencode V2 block. */
|
|
137
|
+
providers: Record<string, OpencodeV2ProviderBlock>;
|
|
89
138
|
}
|
|
90
139
|
|
|
91
140
|
/** Provider key owned by this project; the only key any exporter ever emits. */
|
|
@@ -99,6 +148,21 @@ export const OPENCODE_CONFIG_SCHEMA = "https://opencode.ai/config.json";
|
|
|
99
148
|
*/
|
|
100
149
|
const OPENCODE_PROVIDER_NPM = "@ai-sdk/openai-compatible";
|
|
101
150
|
|
|
151
|
+
/**
|
|
152
|
+
* opencode V2's spelling of the same runtime. V2 resolves providers through its own
|
|
153
|
+
* package table and ignores the V1 `npm` field, so a V2 block has to name this package
|
|
154
|
+
* or the provider is not loaded at all.
|
|
155
|
+
*
|
|
156
|
+
* Verified end-to-end against opencode 0.0.0-beta-18684: `GET /api/model` resolves this
|
|
157
|
+
* package for the provider and applies the per-model `variants`. opencode changes its
|
|
158
|
+
* provider package table between releases, so re-verify the supported range whenever it
|
|
159
|
+
* moves; a stale string breaks only the V2 block, silently.
|
|
160
|
+
*/
|
|
161
|
+
const OPENCODE_V2_PROVIDER_PACKAGE = "@opencode-ai/ai/providers/openai-compatible";
|
|
162
|
+
|
|
163
|
+
/** Display name for the provider block, identical in both generations. */
|
|
164
|
+
const OPENCODE_PROVIDER_NAME = "OpenCodex";
|
|
165
|
+
|
|
102
166
|
/**
|
|
103
167
|
* Env var carrying the proxy admission key to opencode. The config only ever holds the
|
|
104
168
|
* `{env:...}` reference, so the secret never lands on disk. opencode substitutes it at
|
|
@@ -474,6 +538,84 @@ export function primeConfigPath(env: OpencodeLaunchEnv = process.env, home: stri
|
|
|
474
538
|
return join(primeAgentDir(env, home), "models.json");
|
|
475
539
|
}
|
|
476
540
|
|
|
541
|
+
/**
|
|
542
|
+
* Aside's state root. Unlike every other client here, Aside ships NO variable
|
|
543
|
+
* that relocates it: its CLI carries `ASIDE_DAEMON_BASE_URL`,
|
|
544
|
+
* `ASIDE_PRODUCT_VARIANT` and similar, and the only `.aside` path baked into the
|
|
545
|
+
* binary is its own update-check file under `~/.aside/cli`. So there is no
|
|
546
|
+
* client-owned override to mirror, and this registry does not invent one.
|
|
547
|
+
*/
|
|
548
|
+
export function asideHomeDir(_env: OpencodeLaunchEnv = process.env, home: string = homedir()): string {
|
|
549
|
+
return join(home, ".aside");
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Which account's catalog we write.
|
|
554
|
+
*
|
|
555
|
+
* Aside is per-ACCOUNT: state lives under `~/.aside/u/<id>/` and the id comes
|
|
556
|
+
* from `accounts.json`, which Aside maintains. That makes this the only path
|
|
557
|
+
* resolver here that parses file CONTENTS rather than probing existence — the
|
|
558
|
+
* module already does the latter at four sites.
|
|
559
|
+
*
|
|
560
|
+
* It throws rather than defaulting. A machine can hold several accounts (both
|
|
561
|
+
* `u/0` and `u/1` existed on the machine this was developed against), so
|
|
562
|
+
* guessing `0` when the manifest cannot be read would name a real config file
|
|
563
|
+
* belonging to a DIFFERENT account, pass the installed-directory check, and
|
|
564
|
+
* write into somebody else's catalog. An unresolvable account is reported the
|
|
565
|
+
* same way an unresolvable `DSH_HOME` is.
|
|
566
|
+
*
|
|
567
|
+
* Callers that need BOTH the config path and the detect directory must derive
|
|
568
|
+
* them from ONE call to `asideAccountDir` rather than calling the two exported
|
|
569
|
+
* helpers in sequence: `resolveIntegrationPaths` in the integration registry is
|
|
570
|
+
* that seam. Caching here cannot substitute for it — a cache keyed on the
|
|
571
|
+
* manifest's mtime re-reads exactly when the manifest changes, which is the
|
|
572
|
+
* case the consistency is needed for.
|
|
573
|
+
*/
|
|
574
|
+
function asideCurrentAccountId(root: string): number {
|
|
575
|
+
const manifest = join(root, "accounts.json");
|
|
576
|
+
let raw: string;
|
|
577
|
+
try {
|
|
578
|
+
raw = readFileSync(manifest, "utf8");
|
|
579
|
+
} catch {
|
|
580
|
+
throw new ClientPathError(
|
|
581
|
+
`Aside's account manifest is missing or unreadable at ${manifest}, so opencodex cannot tell which `
|
|
582
|
+
+ "account's model catalog to write. Launch Aside once to create it.",
|
|
583
|
+
);
|
|
584
|
+
}
|
|
585
|
+
let parsed: unknown;
|
|
586
|
+
try {
|
|
587
|
+
parsed = JSON.parse(raw);
|
|
588
|
+
} catch {
|
|
589
|
+
throw new ClientPathError(
|
|
590
|
+
`Aside's account manifest at ${manifest} is not readable JSON, so the account it names cannot be `
|
|
591
|
+
+ "trusted. Writing a guessed account would target a different account's catalog.",
|
|
592
|
+
);
|
|
593
|
+
}
|
|
594
|
+
const id = (parsed as { currentAccountId?: unknown } | null)?.currentAccountId;
|
|
595
|
+
if (typeof id !== "number" || !Number.isInteger(id) || id < 0) {
|
|
596
|
+
throw new ClientPathError(
|
|
597
|
+
`Aside's account manifest at ${manifest} declares no usable currentAccountId, so opencodex cannot `
|
|
598
|
+
+ "tell which account is current.",
|
|
599
|
+
);
|
|
600
|
+
}
|
|
601
|
+
return id;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* The signed-in account's directory. This is also the install signal: the CLI
|
|
606
|
+
* creates `~/.aside/cli` for its own update check before any account exists, so
|
|
607
|
+
* the OUTER directory can be present on a machine that never signed in.
|
|
608
|
+
*/
|
|
609
|
+
export function asideAccountDir(env: OpencodeLaunchEnv = process.env, home: string = homedir()): string {
|
|
610
|
+
const root = asideHomeDir(env, home);
|
|
611
|
+
return join(root, "u", String(asideCurrentAccountId(root)));
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/** Aside's custom-provider catalog for the current account. */
|
|
615
|
+
export function asideConfigPath(env: OpencodeLaunchEnv = process.env, home: string = homedir()): string {
|
|
616
|
+
return join(asideAccountDir(env, home), "models.json");
|
|
617
|
+
}
|
|
618
|
+
|
|
477
619
|
/**
|
|
478
620
|
* One proxy-routed model destined for a client config. Deliberately narrower than
|
|
479
621
|
* `CatalogModel` so a serializer cannot reach for a field that does not survive the
|
|
@@ -516,7 +658,8 @@ export type ExportClientId =
|
|
|
516
658
|
| "dsh"
|
|
517
659
|
| "mcode"
|
|
518
660
|
| "zcode"
|
|
519
|
-
| "prime"
|
|
661
|
+
| "prime"
|
|
662
|
+
| "aside";
|
|
520
663
|
|
|
521
664
|
export interface ExportClientSpec {
|
|
522
665
|
id: ExportClientId;
|
|
@@ -651,8 +794,9 @@ function exportModelLabel(model: OpencodeCatalogModel): string {
|
|
|
651
794
|
return `${id} (${providerLabel})`;
|
|
652
795
|
}
|
|
653
796
|
|
|
654
|
-
|
|
655
|
-
|
|
797
|
+
/** Endpoint plus admission, identical for the V1 `options` and V2 `settings` field. */
|
|
798
|
+
function opencodeProviderConnection(baseURL: string, config: OcxConfig): OpencodeProviderConnection {
|
|
799
|
+
const options: OpencodeProviderConnection = { baseURL };
|
|
656
800
|
// Non-loopback binds accept proxy admission only via x-opencodex-api-key so Authorization
|
|
657
801
|
// stays free for Codex Direct upstream credentials when applicable.
|
|
658
802
|
if (shouldInjectApiAuthHeader(config)) {
|
|
@@ -664,37 +808,99 @@ function opencodeProviderOptions(baseURL: string, config: OcxConfig): OpencodePr
|
|
|
664
808
|
}
|
|
665
809
|
|
|
666
810
|
/**
|
|
667
|
-
*
|
|
811
|
+
* Selectable reasoning efforts for one model, in canonical ladder order.
|
|
812
|
+
*
|
|
813
|
+
* No model-level `settings.reasoningEffort` default is emitted: the proxy already applies
|
|
814
|
+
* its own configured default when a request carries no effort, and pinning one here would
|
|
815
|
+
* override a default the user controls in opencodex. Variants are opt-in per selection,
|
|
816
|
+
* which is the same reason we never emit `defaultModel` for MCode.
|
|
817
|
+
*
|
|
818
|
+
* `none` is dropped even when a ladder declares it. It is a valid *declared* effort, but the
|
|
819
|
+
* chat ingress filters wire efforts against `OUTPUT_CONFIG_EFFORTS`, which has no `none`, so
|
|
820
|
+
* selecting it would send no effort at all and silently fall back to the proxy default — a
|
|
821
|
+
* selectable value that cannot do what its label says. Same call MCode makes for its picker.
|
|
822
|
+
*/
|
|
823
|
+
function opencodeEffortVariants(model: OpencodeCatalogModel): OpencodeModelVariant[] | undefined {
|
|
824
|
+
if (model.reasoningEfforts === undefined) return undefined;
|
|
825
|
+
// Canonical order (none, minimal, then low..ultra) and dedupe, so the picker order does
|
|
826
|
+
// not depend on whatever order a provider listed its efforts in.
|
|
827
|
+
const efforts = canonicalizeReasoningEfforts(model.reasoningEfforts).filter(effort => effort !== "none");
|
|
828
|
+
if (efforts.length === 0) return undefined;
|
|
829
|
+
return efforts.map(effort => ({ id: effort, settings: { reasoningEffort: effort } }));
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
/**
|
|
833
|
+
* Both provider generations for one resolved base URL.
|
|
668
834
|
*
|
|
669
835
|
* `limit.context` is emitted ONLY from an authoritative context window — never guessed.
|
|
670
836
|
* When none is available the whole `limit` block is dropped and opencode keeps its own
|
|
671
837
|
* defaults; when one is present, `limit.output` rides along (opencode's schema requires
|
|
672
838
|
* the pair) clamped to the context window.
|
|
839
|
+
*
|
|
840
|
+
* Two blocks instead of one because opencode V2 reads the `providers` map and V1 reads
|
|
841
|
+
* `provider`, and only the V2 form applies `variants`. Emitting both keeps V1 installs
|
|
842
|
+
* working: V2 merges them by provider id and model id, so a model listed in both blocks
|
|
843
|
+
* appears once, with the V2 entry's name, connection, and variants.
|
|
673
844
|
*/
|
|
674
|
-
function
|
|
845
|
+
export function opencodeProviderBlocks(
|
|
675
846
|
baseURL: string,
|
|
676
847
|
catalogModels: readonly OpencodeCatalogModel[],
|
|
677
848
|
config: OcxConfig,
|
|
678
|
-
):
|
|
679
|
-
const
|
|
849
|
+
): OpencodeProviderBlocks {
|
|
850
|
+
const v1Models: Record<string, OpencodeModelEntry> = {};
|
|
851
|
+
const v2Models: Record<string, OpencodeV2ModelEntry> = {};
|
|
680
852
|
for (const model of catalogModels) {
|
|
681
853
|
const key = model.namespaced;
|
|
682
|
-
if (
|
|
854
|
+
if (v1Models[key]) continue; // first entry wins; native rows lead /api/models
|
|
683
855
|
const entry: OpencodeModelEntry = { name: exportModelLabel(model) };
|
|
684
856
|
const context = authoritativeContextWindow(model.contextWindow);
|
|
685
857
|
if (context !== undefined) {
|
|
686
858
|
entry.limit = { context, output: outputBudgetFor(context) };
|
|
687
859
|
}
|
|
688
|
-
|
|
860
|
+
v1Models[key] = entry;
|
|
861
|
+
const variants = opencodeEffortVariants(model);
|
|
862
|
+
// Own `limit` object, not a shared reference: the two blocks are serialized and reasoned
|
|
863
|
+
// about separately, and an in-place edit of one must never move the other.
|
|
864
|
+
v2Models[key] = {
|
|
865
|
+
...entry,
|
|
866
|
+
...(entry.limit ? { limit: { ...entry.limit } } : {}),
|
|
867
|
+
...(variants ? { variants } : {}),
|
|
868
|
+
};
|
|
689
869
|
}
|
|
690
870
|
return {
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
871
|
+
v1: {
|
|
872
|
+
npm: OPENCODE_PROVIDER_NPM,
|
|
873
|
+
name: OPENCODE_PROVIDER_NAME,
|
|
874
|
+
options: opencodeProviderConnection(baseURL, config),
|
|
875
|
+
models: v1Models,
|
|
876
|
+
},
|
|
877
|
+
v2: {
|
|
878
|
+
package: OPENCODE_V2_PROVIDER_PACKAGE,
|
|
879
|
+
name: OPENCODE_PROVIDER_NAME,
|
|
880
|
+
settings: opencodeProviderConnection(baseURL, config),
|
|
881
|
+
models: v2Models,
|
|
882
|
+
},
|
|
695
883
|
};
|
|
696
884
|
}
|
|
697
885
|
|
|
886
|
+
/** `opencodex` provider block for a resolved base URL (opencode V1 shape). */
|
|
887
|
+
function opencodeProviderBlock(
|
|
888
|
+
baseURL: string,
|
|
889
|
+
catalogModels: readonly OpencodeCatalogModel[],
|
|
890
|
+
config: OcxConfig,
|
|
891
|
+
): OpencodeProviderBlock {
|
|
892
|
+
return opencodeProviderBlocks(baseURL, catalogModels, config).v1;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
/** `opencodex` provider block for a resolved base URL (opencode V2 shape, carries variants). */
|
|
896
|
+
export function opencodeV2ProviderBlock(
|
|
897
|
+
baseURL: string,
|
|
898
|
+
catalogModels: readonly OpencodeCatalogModel[],
|
|
899
|
+
config: OcxConfig = OPENCODE_PROVIDER_BLOCK_DEFAULT_CONFIG,
|
|
900
|
+
): OpencodeV2ProviderBlock {
|
|
901
|
+
return opencodeProviderBlocks(baseURL, catalogModels, config).v2;
|
|
902
|
+
}
|
|
903
|
+
|
|
698
904
|
/**
|
|
699
905
|
* Build the `opencodex` provider block from proxy catalog rows keyed by each row's
|
|
700
906
|
* canonical `namespaced` selector. Used by the `ocx opencode` launcher, which injects
|
|
@@ -726,14 +932,23 @@ export function normalizeExportModels(models: readonly ExportModel[]): ExportMod
|
|
|
726
932
|
return unique.sort((a, b) => (a.namespaced < b.namespaced ? -1 : a.namespaced > b.namespaced ? 1 : 0));
|
|
727
933
|
}
|
|
728
934
|
|
|
729
|
-
/**
|
|
935
|
+
/**
|
|
936
|
+
* OpenCode document: both provider generations plus `$schema`, and nothing else.
|
|
937
|
+
*
|
|
938
|
+
* The order below fixes the order of the emitted keys and nothing else: the two blocks are
|
|
939
|
+
* disjoint top-level keys, and which generation opencode prefers when it merges them is
|
|
940
|
+
* opencode's decision, not a consequence of where we write it. Both blocks are generated in
|
|
941
|
+
* one pass so they cannot disagree about the model set, the names, or the connection.
|
|
942
|
+
*/
|
|
730
943
|
function buildOpencodeClientConfig(ctx: ExportContext): OpencodeGeneratedConfig {
|
|
731
|
-
const
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
944
|
+
const models = normalizeExportModels(ctx.models);
|
|
945
|
+
const config = ctx.config ?? OPENCODE_PROVIDER_BLOCK_DEFAULT_CONFIG;
|
|
946
|
+
const blocks = opencodeProviderBlocks(ctx.baseUrl, models, config);
|
|
947
|
+
return {
|
|
948
|
+
$schema: OPENCODE_CONFIG_SCHEMA,
|
|
949
|
+
provider: { [OPENCODE_PROVIDER_ID]: blocks.v1 },
|
|
950
|
+
providers: { [OPENCODE_PROVIDER_ID]: blocks.v2 },
|
|
951
|
+
};
|
|
737
952
|
}
|
|
738
953
|
|
|
739
954
|
export interface PiModelEntry {
|
|
@@ -1435,7 +1650,16 @@ function singleFragment(clientId: ExportClientId, path: readonly string[], value
|
|
|
1435
1650
|
|
|
1436
1651
|
function buildOpencodeContribution(ctx: ExportContext): ManagedContribution {
|
|
1437
1652
|
const doc = buildOpencodeClientConfig(ctx);
|
|
1438
|
-
return
|
|
1653
|
+
return {
|
|
1654
|
+
clientId: "opencode",
|
|
1655
|
+
fragments: [
|
|
1656
|
+
// Legacy block first, so the emitted JSON reads the way a config migration does.
|
|
1657
|
+
// opencode V1 reads only `provider`, V2 reads both, and the generation that wins the
|
|
1658
|
+
// merge is decided by opencode — what we control is that both name the same models.
|
|
1659
|
+
{ path: ["provider", OPENCODE_PROVIDER_ID], value: doc.provider[OPENCODE_PROVIDER_ID] },
|
|
1660
|
+
{ path: ["providers", OPENCODE_PROVIDER_ID], value: doc.providers[OPENCODE_PROVIDER_ID] },
|
|
1661
|
+
],
|
|
1662
|
+
};
|
|
1439
1663
|
}
|
|
1440
1664
|
|
|
1441
1665
|
function buildPiContribution(ctx: ExportContext): ManagedContribution {
|
|
@@ -1512,6 +1736,29 @@ function buildPrimeContribution(ctx: ExportContext): ManagedContribution {
|
|
|
1512
1736
|
return singleFragment("prime", ["providers", OPENCODE_PROVIDER_ID], doc.providers[OPENCODE_PROVIDER_ID]);
|
|
1513
1737
|
}
|
|
1514
1738
|
|
|
1739
|
+
/**
|
|
1740
|
+
* Aside is the strongest case yet for reusing Pi's builder, because the
|
|
1741
|
+
* evidence is a live file rather than a package manifest.
|
|
1742
|
+
*
|
|
1743
|
+
* The machine this landed on already had opencodex wired into Aside BY HAND:
|
|
1744
|
+
* `~/.aside/u/0/models.json` carried a `providers.opencodex` block with the same
|
|
1745
|
+
* four keys, the same `openai-completions` dialect, the same
|
|
1746
|
+
* `opencodex-loopback` placeholder, and 24 models using the same
|
|
1747
|
+
* `thinkingLevelMap` levels this builder emits. A user reproduced Pi's document
|
|
1748
|
+
* from scratch because that is what Aside reads.
|
|
1749
|
+
*
|
|
1750
|
+
* Key ORDER differs (the hand-written file has `apiKey` before `api`), which is
|
|
1751
|
+
* why the devlog claims compatibility rather than byte equality: JSON key order
|
|
1752
|
+
* is not semantic and Aside parses this file rather than diffing it.
|
|
1753
|
+
*
|
|
1754
|
+
* As with prime, only the ownership stamp is Aside's own, so a disable removes
|
|
1755
|
+
* the fragment this client recorded and not one another client wrote.
|
|
1756
|
+
*/
|
|
1757
|
+
function buildAsideContribution(ctx: ExportContext): ManagedContribution {
|
|
1758
|
+
const doc = buildPiClientConfig(ctx);
|
|
1759
|
+
return singleFragment("aside", ["providers", OPENCODE_PROVIDER_ID], doc.providers[OPENCODE_PROVIDER_ID]);
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1515
1762
|
export const EXPORT_CLIENTS: Record<ExportClientId, ExportClientSpec> = {
|
|
1516
1763
|
opencode: {
|
|
1517
1764
|
id: "opencode",
|
|
@@ -1665,6 +1912,24 @@ export const EXPORT_CLIENTS: Record<ExportClientId, ExportClientSpec> = {
|
|
|
1665
1912
|
// from this initial loopback-only integration — same stance as OMP's.
|
|
1666
1913
|
loopbackOnly: true,
|
|
1667
1914
|
},
|
|
1915
|
+
aside: {
|
|
1916
|
+
id: "aside",
|
|
1917
|
+
// Not a bare `models.json`: a download lands in the user's Downloads folder,
|
|
1918
|
+
// where pi's and prime's files would collide with it. Prime set this
|
|
1919
|
+
// precedent with `prime-models.json`.
|
|
1920
|
+
filename: "aside-models.json",
|
|
1921
|
+
destination: env => asideConfigPath(env),
|
|
1922
|
+
apiKeyEnv: "",
|
|
1923
|
+
exportHint: "Aside reads a non-secret placeholder from models.json; loopback needs no key.",
|
|
1924
|
+
build: buildPiClientConfig,
|
|
1925
|
+
format: "json",
|
|
1926
|
+
summarize: summarizePi,
|
|
1927
|
+
buildContribution: buildAsideContribution,
|
|
1928
|
+
// The observed provider block has exactly four keys and none is `headers`,
|
|
1929
|
+
// so the dedicated admission header has nowhere to live and a non-loopback
|
|
1930
|
+
// bind would generate a config that 401s.
|
|
1931
|
+
loopbackOnly: true,
|
|
1932
|
+
},
|
|
1668
1933
|
};
|
|
1669
1934
|
|
|
1670
1935
|
export const EXPORT_CLIENT_IDS: readonly ExportClientId[] = Object.keys(EXPORT_CLIENTS) as ExportClientId[];
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
} from "../config";
|
|
13
13
|
import { assertNotRealHomeUnderTest } from "../lib/test-home-guard";
|
|
14
14
|
import type { CodexAccountCredentialRecord, CodexAccountCredentials } from "../types";
|
|
15
|
+
import { advanceCodexCredentialMutationEpoch } from "./credential-mutation-epoch";
|
|
15
16
|
|
|
16
17
|
type LegacyCodexAccountStore = Record<string, CodexAccountCredentials>;
|
|
17
18
|
type CodexAccountStore = Record<string, CodexAccountCredentialRecord>;
|
|
@@ -111,6 +112,11 @@ function persist(store: CodexAccountStore): void {
|
|
|
111
112
|
atomicWriteFile(codexAccountsPath(), JSON.stringify(store, null, 2) + "\n");
|
|
112
113
|
}
|
|
113
114
|
|
|
115
|
+
function persistCredentialMutation(store: CodexAccountStore): void {
|
|
116
|
+
persist(store);
|
|
117
|
+
advanceCodexCredentialMutationEpoch();
|
|
118
|
+
}
|
|
119
|
+
|
|
114
120
|
function preservedValidationMetadata(record: CodexAccountCredentialRecord | undefined): Pick<
|
|
115
121
|
CodexAccountCredentialRecord,
|
|
116
122
|
"lastCodexValidatedAt" | "lastCodexValidationStatus" | "lastCodexValidationError"
|
|
@@ -142,7 +148,7 @@ export function saveCodexAccountCredential(id: string, cred: CodexAccountCredent
|
|
|
142
148
|
replacedAt: current ? Date.now() : undefined,
|
|
143
149
|
...preservedValidationMetadata(current),
|
|
144
150
|
};
|
|
145
|
-
|
|
151
|
+
persistCredentialMutation(store);
|
|
146
152
|
});
|
|
147
153
|
}
|
|
148
154
|
|
|
@@ -213,7 +219,7 @@ export function saveCodexAccountCredentialIfGeneration(
|
|
|
213
219
|
replacedAt: current.replacedAt,
|
|
214
220
|
...preservedValidationMetadata(current),
|
|
215
221
|
};
|
|
216
|
-
|
|
222
|
+
persistCredentialMutation(store);
|
|
217
223
|
return true;
|
|
218
224
|
});
|
|
219
225
|
}
|
|
@@ -304,7 +310,7 @@ export function commitRefreshedCodexCredentialWithAliases(
|
|
|
304
310
|
propagatedAliases.push({ id: aliasId, generation: aliasGeneration });
|
|
305
311
|
}
|
|
306
312
|
}
|
|
307
|
-
|
|
313
|
+
persistCredentialMutation(store);
|
|
308
314
|
return { committed: true, propagatedAliases };
|
|
309
315
|
});
|
|
310
316
|
}
|
|
@@ -315,7 +321,7 @@ export function tombstoneCodexAccount(id: string): number {
|
|
|
315
321
|
const current = store[id];
|
|
316
322
|
const generation = (current?.generation ?? 0) + 1;
|
|
317
323
|
store[id] = { generation, deletedAt: Date.now() };
|
|
318
|
-
|
|
324
|
+
persistCredentialMutation(store);
|
|
319
325
|
return generation;
|
|
320
326
|
});
|
|
321
327
|
}
|
|
@@ -90,9 +90,9 @@ export function deriveStartupHealth(inputs: StartupHealthInputs): StartupHealth
|
|
|
90
90
|
: inputs.routingKind === "custom-local" || inputs.routingKind === "unknown"
|
|
91
91
|
? COMMANDS.restoreNative
|
|
92
92
|
: inputs.serviceSupported
|
|
93
|
-
// An already-registered service is refreshed in place: `repair`
|
|
94
|
-
//
|
|
95
|
-
// cannot switch a WinSW install to Task Scheduler the way `install` would. Only a
|
|
93
|
+
// An already-registered service is refreshed in place: `repair` reuses healthy Windows
|
|
94
|
+
// scheduler definitions, while stale ones may be re-registered and require elevation.
|
|
95
|
+
// It still cannot switch a WinSW install to Task Scheduler the way `install` would. Only a
|
|
96
96
|
// genuinely absent (or conflicting, which needs uninstall-then-install) service
|
|
97
97
|
// gets the registering command.
|
|
98
98
|
? (inputs.serviceInstalled && !inputs.serviceConflict ? COMMANDS.repairService : COMMANDS.installService)
|
|
@@ -42,6 +42,7 @@ import { effectiveGoogleMode, getProviderRegistryEntry, providerMatchesRegistryT
|
|
|
42
42
|
import { parseAntigravityAvailableModels, registerAntigravityDiscoveredWireModels } from "../../providers/antigravity-models";
|
|
43
43
|
import { applyProviderContextCap, providerContextCap, resolveUnknownRoutedContextWindow } from "../../providers/context-cap";
|
|
44
44
|
import { clampAutoCompactTokenLimit } from "../../providers/auto-compact-budget";
|
|
45
|
+
import { effectiveModelAliases } from "../../providers/default-aliases";
|
|
45
46
|
import { routedSlug, slugEquals, slugEquivalenceKey, slugsEquivalent } from "../../providers/slug-codec";
|
|
46
47
|
import { CODEX_GPT5_IDENTITY_LINE } from "../../adapters/identity";
|
|
47
48
|
import { filterCursorConfiguredModelsByLiveDiscovery } from "../../adapters/cursor/discovery";
|
|
@@ -2151,6 +2152,21 @@ async function gatherRoutedModelsUncached(
|
|
|
2151
2152
|
// Custom rows override discovered rows that encode to the same Codex-facing slug.
|
|
2152
2153
|
const customKeys = new Set(customModels.map(c => routedSlug(c.provider, c.id)));
|
|
2153
2154
|
const deduped = all.filter(m => !customKeys.has(routedSlug(m.provider, m.id)));
|
|
2155
|
+
const models = [...deduped, ...customModels];
|
|
2156
|
+
// ponytail: catalog-scale scan; index ids by provider if catalog growth makes this measurable.
|
|
2157
|
+
const aliasDisplayNames = new Map(activeProviders.flatMap(({ name, provider }) => {
|
|
2158
|
+
const providerModels = models.filter(model => model.provider === name);
|
|
2159
|
+
const aliases = [...effectiveModelAliases(config, provider, providerModels.map(model => model.id))];
|
|
2160
|
+
return aliases.flatMap(([id, { alias }]) => {
|
|
2161
|
+
const exact = providerModels.filter(model => model.id === id);
|
|
2162
|
+
const matches = exact.length > 0
|
|
2163
|
+
? exact
|
|
2164
|
+
: providerModels.filter(model => model.id.toLowerCase() === id.toLowerCase());
|
|
2165
|
+
return matches.length === 1
|
|
2166
|
+
? [[`${name}/${matches[0]!.id}`, `${provider.alias || name}/${alias}`] as const]
|
|
2167
|
+
: [];
|
|
2168
|
+
});
|
|
2169
|
+
}));
|
|
2154
2170
|
const providerModelOutcomes = providerResults.map(result => (
|
|
2155
2171
|
result.outcome.provider === OPENAI_API_PROVIDER_ID
|
|
2156
2172
|
&& capture.openAiApiPolicy.state === "captured"
|
|
@@ -2159,7 +2175,10 @@ async function gatherRoutedModelsUncached(
|
|
|
2159
2175
|
: result.outcome
|
|
2160
2176
|
));
|
|
2161
2177
|
return {
|
|
2162
|
-
models:
|
|
2178
|
+
models: models.map(model => {
|
|
2179
|
+
const displayName = aliasDisplayNames.get(`${model.provider}/${model.id}`);
|
|
2180
|
+
return displayName && !model.displayName ? { ...model, displayName } : model;
|
|
2181
|
+
}),
|
|
2163
2182
|
comboOmissions: localOmissions,
|
|
2164
2183
|
providerAuthOutcomes: localProviderAuthOutcomes,
|
|
2165
2184
|
providerModelOutcomes,
|
|
@@ -35,6 +35,7 @@ import { codexAccountNamespaceEntries, isMainCodexAccountTarget } from "../accou
|
|
|
35
35
|
import { MAIN_CODEX_ACCOUNT_ID } from "../main-account";
|
|
36
36
|
import {
|
|
37
37
|
availableAccountGatedNativeModels,
|
|
38
|
+
codexModelEntitlementStateForAccount,
|
|
38
39
|
isCodexModelEntitlementSnapshotCurrent,
|
|
39
40
|
resolveCodexModelEntitlements,
|
|
40
41
|
type CodexModelEntitlementSnapshot,
|
|
@@ -1613,10 +1614,10 @@ function writeRetainedCatalogSync({
|
|
|
1613
1614
|
? new Map([...accountBoundNativeOpenAiSlugsBySelector(config, observedAccountNativeEntries)].map(([selector, slugs]) => {
|
|
1614
1615
|
const target = accountTargets.get(selector);
|
|
1615
1616
|
const accountId = target && isMainCodexAccountTarget(target) ? MAIN_CODEX_ACCOUNT_ID : target;
|
|
1616
|
-
const entitled = accountId ? modelEntitlements.modelsByAccount.get(accountId) : undefined;
|
|
1617
|
-
const confirmed = accountId ? modelEntitlements.confirmedAccountIds.has(accountId) : false;
|
|
1618
1617
|
return [selector, slugs.filter(slug => (
|
|
1619
|
-
!ACCOUNT_GATED_NATIVE_OPENAI_MODELS.has(slug)
|
|
1618
|
+
!ACCOUNT_GATED_NATIVE_OPENAI_MODELS.has(slug)
|
|
1619
|
+
|| (accountId !== undefined
|
|
1620
|
+
&& codexModelEntitlementStateForAccount(modelEntitlements, accountId, slug) === "granted")
|
|
1620
1621
|
))] as const;
|
|
1621
1622
|
}))
|
|
1622
1623
|
: new Map<string, readonly string[]>();
|